Skip to content

Dev Shell (CT 107)

Status: Deployed ✅ Deployed: 2026-05-12 Purpose: Always-on Linux dev box for running Claude Code (and other CLI dev tooling) from anywhere on the tailnet, so the owner's desktop doesn't have to stay on. Tailnet name: dev-shell (IP 100.109.125.96 at provisioning; Tailscale assigns it permanently per device)


Why this exists

The owner runs long Claude Code sessions for the TCG platform + homelab work. Keeping a desktop powered on for that — especially overnight or while traveling — is wasteful and impractical. CT 107 takes the dev-box role: a Debian 12 LXC with Node 22, Docker, the Claude Code CLI, and Tailscale SSH, sized for monorepo work. From anywhere on the tailnet (laptop, phone, work machine), ssh dev@dev-shell lands in a persistent tmux session.

This is the first CT in the homelab that exists purely to serve the operator's dev workflow rather than a customer-facing or platform service. Treat it more like a personal workstation than an application server: it's fine to install new tools ad-hoc, no homelab/ PR required for routine package additions.


Deployment Details

Property Value
CT ID 107
Hostname dev-shell
LAN IP 192.168.0.57
Tailnet IP 100.109.125.96 (assigned 2026-05-12)
CPU cores 4
RAM 8192 MB
Swap 2048 MB
Disk 60 GB (local-lvm)
DNS 1.1.1.1 (router DNS has known lookup issues — same as CT 103 / 105)
Unprivileged yes
Features nesting=1 (required for Docker-in-LXC)
Extra device /dev/net/tun exposed via lxc.cgroup2.devices.allow + lxc.mount.entry (required for Tailscale; unprivileged LXCs don't get TUN by default)
OS Debian 12
Default user dev (sudo NOPASSWD + docker group)
Auth SSH keys (Proxmox root authorized_keys forwarded at create-time). Encrypted transport via Tailscale. Tailscale's --ssh mode is off by default on this box — see "Step 5" for why.

What's installed

Package Version at provisioning Notes
Node.js 22.22.2 NodeSource APT repo (setup_22.x) — matches tcg-platform CI's Node version
Yarn 1.22.22 (system) Corepack enabled; yarn@4.x auto-activates from any repo with packageManager field in package.json
Docker 29.4.3 Convenience-script install (https://get.docker.com) — same pattern as CT 105
Claude Code 2.1.139 npm install -g @anthropic-ai/claude-code
Tailscale 1.96.4 --ssh mode — no need to manage authorized_keys per device
tmux 3.3a For persistent sessions across SSH disconnects
Plus git, curl, vim, htop, jq, build-essential, ca-certificates, gnupg, sudo Standard dev surface

Deployment Plan

Step 1 — Create LXC

From the Proxmox host (192.168.0.200):

pct create 107 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst \
  --hostname dev-shell \
  --cores 4 \
  --memory 8192 \
  --swap 2048 \
  --rootfs local-lvm:60 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.0.57/24,gw=192.168.0.1,firewall=1 \
  --nameserver 1.1.1.1 \
  --features nesting=1 \
  --onboot 1 \
  --unprivileged 1 \
  --ssh-public-keys /root/.ssh/authorized_keys \
  --start 1

Step 2 — Expose /dev/net/tun for Tailscale

Tailscale needs a TUN device. Unprivileged LXCs don't get one by default; expose it explicitly:

pct stop 107
cat >> /etc/pve/lxc/107.conf <<'EOF'
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir
EOF
pct start 107
pct exec 107 -- ls -la /dev/net/tun   # expect: crw-rw-rw- ... 10, 200 /dev/net/tun

Don't skip Step 2

Without /dev/net/tun exposed, tailscaled will crashloop with CreateTUN("tailscale0") failed; /dev/net/tun does not exist. The fix has to happen at the LXC config level, not inside the container.

Step 3 — Install toolchain

pct exec 107 -- bash -c '
  apt-get update -qq
  apt-get install -y -qq curl ca-certificates gnupg sudo vim tmux htop jq git build-essential
  # Docker
  curl -fsSL https://get.docker.com | sh
  # Node 22
  curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
  apt-get install -y -qq nodejs
  # Tailscale
  curl -fsSL https://tailscale.com/install.sh | sh
  # Yarn 4 via corepack
  corepack enable
  # Claude Code globally
  npm install -g @anthropic-ai/claude-code
'

Step 4 — Create dev user

pct exec 107 -- bash -c '
  useradd -m -s /bin/bash dev
  usermod -aG sudo,docker dev
  echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
  mkdir -p /home/dev/.ssh
  cp /root/.ssh/authorized_keys /home/dev/.ssh/authorized_keys
  chmod 700 /home/dev/.ssh
  chmod 600 /home/dev/.ssh/authorized_keys
  chown -R dev:dev /home/dev/.ssh
'

Step 5 — Authenticate Tailscale

pct exec 107 -- tailscale up --hostname=dev-shell
# Prints: "To authenticate, visit: https://login.tailscale.com/a/XXXX"
# Open the URL in a browser, sign in, click "Connect device".
# The pct exec command returns once the device is registered.

# Disable Tailscale's in-process SSH server — see warning below
pct exec 107 -- tailscale set --ssh=false

Don't use tailscale up --ssh on this box (without an ACL update)

Tailscale's --ssh flag enables an in-process SSH server on port 22 over the tailscale0 interface. With the default tailnet ACL (which has no ssh block), the in-process server accepts connections but doesn't actually serve them — it silently swallows TCP traffic to port 22 on the Tailscale IP, while the regular OpenSSH daemon on *:22 stops seeing those packets. Result: ssh dev@dev-shell hangs with no log entry anywhere; LAN-direct ssh dev@192.168.0.57 keeps working. (Initial provisioning ran into this on 2026-05-12; the fix is tailscale set --ssh=false after tailscale up.) If you ever want Tailscale SSH proper, edit the tainet ACL in the admin console to grant an ssh rule first, then re-enable.

Auth model after this step

Plain OpenSSH on port 22, key-based, with Tailscale providing encrypted transport + MagicDNS. From any tailnet device: ssh dev@dev-shell works. From LAN: ssh dev@192.168.0.57 also works. Add new operator keys to /home/dev/.ssh/authorized_keys the same way as any other Linux box.

Step 6 — Login greeting + aliases

pct exec 107 -- bash -c 'cat > /home/dev/.bashrc.local <<EOF
if [ -n "\$PS1" ] && [ -z "\$TMUX" ] && [ -t 1 ]; then
  echo
  echo "  dev-shell on tailnet: \$(tailscale ip -4 2>/dev/null | head -1)"
  echo "  Persistent tmux:  tmux new -A -s dev"
  echo "  Start Claude:     claude  (inside tmux)"
  echo "  Detach session:   Ctrl+b d   (your session keeps running)"
  echo
fi

alias ll="ls -lah"
alias dc="docker compose"

# Headroom for big monorepo builds
export NODE_OPTIONS="--max-old-space-size=6144"
EOF
chown dev:dev /home/dev/.bashrc.local
grep -q "bashrc.local" /home/dev/.bashrc || echo "[ -f ~/.bashrc.local ] && source ~/.bashrc.local" >> /home/dev/.bashrc
'

Step 7 — Verify from a remote tailnet device

# From your laptop / phone / wherever, anywhere on the internet:
ssh dev@dev-shell        # via Tailscale SSH; no port number, no IP needed
# inside the session:
tmux new -A -s dev       # start or reattach the persistent session
claude                   # fire up Claude Code
# Ctrl+b d to detach; the session keeps running after you close ssh

Operational notes

Workflow

The intended pattern is: one tmux session named dev, attach with tmux new -A -s dev. Run Claude Code, dev servers, builds, etc. inside it. Detach with Ctrl+b d whenever you're done — the session keeps running on the LXC, no state lost. Reattach next time you SSH in.

# Standard cycle
ssh dev@dev-shell
tmux new -A -s dev       # creates or reattaches
# work...
# Ctrl+b d
exit

For longer-running things (yarn install of a fresh monorepo, a multi-hour Claude run, etc.) put them in their own tmux window (Ctrl+b c) so you can switch (Ctrl+b 0/1/2…) without killing anything.

Cloning project repos

The default SSH key on the LXC is the Proxmox root key, which is not registered with GitHub. To clone any private repo:

  1. Generate a per-host SSH key on dev-shell: ssh-keygen -t ed25519 -C dev-shell (as the dev user).
  2. Add the resulting ~/.ssh/id_ed25519.pub to your GitHub account's SSH keys.
  3. Or use gh auth login from the dev user — installs a GitHub CLI token in ~/.config/gh/ and gh repo clone handles auth transparently.

The latter is simpler and gives you a working gh for PRs in the same step.

Disk pressure

60 GB is plenty for the current workload (the tcg-platform-source monorepo's node_modules/ peaks around 5–8 GB; a fresh Docker build of Medusa adds ~13 GB but that's only when actively rebuilding). If it ever gets tight, run docker system prune -af (no --volumes) — but unlike CT 105, this CT has no irreplaceable Docker volumes, so --volumes is fine here too if you're sure nothing important is mounted in.

Resizing

# Bump RAM (no reboot needed for unprivileged LXC)
pct set 107 --memory 16384

# Bump disk (online if possible, or offline)
pct resize 107 rootfs +20G

# Bump CPU (no reboot needed)
pct set 107 --cores 6

The Proxmox host has headroom for all three.

Snapshots

Snapshots are cheap on this host; take one before any risky one-off change:

pct snapshot 107 <name> --description '...'

No mandatory snapshot regime — this isn't carrying production data. The dev user's home dir is the only thing worth preserving; mirror anything important to a Git repo or to S3 / Backblaze rather than relying on Proxmox snapshots.


Troubleshooting

Problem Cause Fix
tailscaled crashloops with CreateTUN ... /dev/net/tun does not exist Unprivileged LXC has no TUN device Add lxc.cgroup2.devices.allow: c 10:200 rwm + lxc.mount.entry: /dev/net dev/net none bind,create=dir to /etc/pve/lxc/107.conf, restart the CT
ssh dev@dev-shell from tailnet hangs at TCP connect (Connection refused or just timeout) Tailscale's --ssh is on but the tainet ACL has no ssh block — Tailscale intercepts port 22 on the tailscale0 interface and black-holes; sshd never sees the packet (no log). LAN-direct SSH (ssh dev@192.168.0.57) keeps working because that doesn't go through tailscale0. pct exec 107 -- tailscale set --ssh=false. Verify with tailscale debug prefs \| grep RunSSH (expect false). If you actually want Tailscale SSH, add an ssh block to the tainet ACL in the Tailscale admin console first.
ssh dev@dev-shell hits "Permission denied (publickey)" Key not present in /home/dev/.ssh/authorized_keys Append the client's pubkey: ssh-copy-id dev@dev-shell from the client, or paste manually
corepack complains about Yarn version inside the tcg-platform repo Network blocked, or the corepack cache is stale corepack prepare yarn@4.12.0 --activate as dev user; corepack will refetch from npm registry
Docker commands hang after host reboot LXC restarted before docker.service did inside it sudo systemctl restart docker from inside the CT; or set --onboot 1 for ordered start (already set)
Disk fills up unexpectedly during a yarn install The monorepo's lockfile churn + multiple .next/ caches du -sh ~/* to find the culprit; apps/dashboard/.next/ is the usual ~2 GB suspect, safe to delete (regenerable)

Updates needed elsewhere (post-deploy)

  • [ ] Add dev-shell to homelab/00_secrets/Credentials Index.md (root SSH key reference, dev user pw if you set one, GitHub token if you provision one)
  • [ ] Update homelab/01_planning/Phase 1/Architecture Diagram.md — add CT 107 to the IP map and Mermaid diagram
  • [ ] Add CT 107 to homelab/02_setup_logs/Phase 1 Health Checks.md if you maintain a CT inventory there
  • [ ] Update Homepage services.yaml on CT 103 if you want a card linking to the box (internal LAN only — point at http://192.168.0.57 or just list as informational with no widget)