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:
- Generate a per-host SSH key on dev-shell:
ssh-keygen -t ed25519 -C dev-shell(as thedevuser). - Add the resulting
~/.ssh/id_ed25519.pubto your GitHub account's SSH keys. - Or use
gh auth loginfrom thedevuser — installs a GitHub CLI token in~/.config/gh/andgh repo clonehandles 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-shelltohomelab/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.mdif you maintain a CT inventory there - [ ] Update Homepage
services.yamlon CT 103 if you want a card linking to the box (internal LAN only — point athttp://192.168.0.57or just list as informational with no widget)