Dev · arm64 · Ubuntu 26.04 · 6 programs

Development Environment

A Mac on the desk, an Ubuntu VM beside it, and the homelab behind both.

06

Desk

What is on the Mac

macOS runs the editor and little else that matters. VS Code and a terminal both arrive over SSH on localhost:5555, which the container runtime forwards into the VM; the shell, the toolchains and Claude Code all live on the far side of that port.

Editor
VS Code

Remote-SSH. The server side runs in the VM; the window on the Mac is a view of it.

Shell
Terminal

ssh -p 5555 coder@127.0.0.1 — the same door, without the editor.

sshd · localhost:5555
Into the VM

Port 22 inside, forwarded by the container runtime. Holds the door until /work answers, for up to 120 s.

Also on the desk
Agent
Claude Code

Runs inside the sandbox, on the same shell. FLEET.md is imported into every session, so it knows the constraints.

Runtime
Apple container

Creates the VM from spawn, starts it again at each login, and builds the image from the tarball.

05

Sandbox

One VM, made from a tarball

An Apple container VM called dev: arm64 Ubuntu 26.04, six CPUs, 24 GB, and a rebuilt 6.18.15 kernel with netem and the shaping qdiscs compiled in. All of it comes from ../devcontainer — make packs the sources, the Mac builds the image, and spawn creates the VM with its volumes, binds, MTU and port.

In the sandbox
../devcontainer

A Dockerfile, a dozen scripts, supervisor.conf, the mise manifest, the kernel.

In the sandbox
make

Checks every COPY is covered and packs one reproducible tarball.

On the Mac
container build

Turns the tarball into the localdev image.

On the Mac
spawn

container run: CPUs, memory, five volumes, two binds, the kernel, port 5555.

Apple container VM
dev
arm64Ubuntu 26.046 CPU24 GBkernel 6.18.15 + netemlink MTU 1500

No Docker daemon and none wanted; no qemu; /proc/sys read-only. Anything that needs another architecture goes to the fleet builder instead.

04

Boot

entrypoint, then supervisord

entrypoint.sh makes a fresh container safe to use — hands the empty volumes to coder, pins per-route MTUs, restores the metadata database if it is missing — and execs supervisord, which starts six programs by priority. Order is a preference, not a dependency; the one hard wait is sshd holding the door until /work answers.

01 · Before supervisord
entrypoint.sh

Hands the empty volumes to coder, pins the route MTUs, restores the metadata DB if it is missing, then execs supervisord.

02 · Priority 1
juicefs

Mounts /work from the object store. Exits 78 and parks if the metadata is missing — no restart fixes that.

03 · Priority 2
litestream

Streams the metadata SQLite to its own bucket. Parks with instructions if the credentials file is absent.

04 · Priority 5 · oneshot
toolchains

Converges /opt/tc against the manifest and exits. EXITED is its healthy state.

05 · Priority 10
buildkitd

The sandbox’s native arm64 build node, half of the fleet builder.

06 · Priority 20
fstrim

Trims every ext4 volume at boot and daily, so the sparse images on the Mac shrink back. Skips a volume with the error flag set.

07 · Priority 30
sshd

Last. Waits for the JuiceFS mount before accepting logins, so a reattaching VS Code window never opens on an empty tree.

03

Toolchains

A manifest in the image, payload on a volume

The image carries mise and its manifest; toolchains.sh installs what the manifest names onto the toolchains volume, so bumping Node is an edit plus a oneshot, not a rebuild. Single static binaries stay in the image. /etc/environment puts all of it on PATH for non-interactive sessions too, the VS Code server included.

Via mise, on the toolchains volume
GoNodePython + pipPulumiuvkubectl

Pinned in /etc/mise/config.toml. kubectl tracks the cluster, not the image.

In the image, static
helmyqjqghripgrepprotocdocker CLI + buildxbuildkitdjuicefslitestream

Single binaries with no moving target to track.

Not installed
rustjavabundenoterraform

Go, npm, pip, uv and everything XDG-conformant write to the devcache volume, never to the home directory.

02

Storage

Split by how you get it back

Five volumes, divided by recoverability rather than by content: identity worth snapshotting, toolchains rebuildable in minutes, caches that are pure churn. Projects live on JuiceFS at /work — data in the homelab’s object store, metadata in a SQLite file on a bind from the Mac, streamed to a bucket by litestream.

Volume · homedir
/home/coder
Lose it and… it hurts

Identity and configuration only, small enough to snapshot.

Volume · toolchains
/opt/tc
Lose it and… minutes

One oneshot puts it back from the manifest.

Volume · devcache
/var/cache/dev
Lose it and… one slow rebuild

Build and package caches. Never back this up.

Volume · buildkitcache
/var/lib/buildkit
Lose it and… one slow first build

A volume, not a bind: overlayfs needs a real Linux filesystem underneath.

Volume · sshkeys
/etc/ssh/hostkeys
Lose it and… a known_hosts mismatch

Host fingerprints stay stable across image rebuilds.

JuiceFS
/work

~/code is a symlink to it. Metadata operations are slow here; never walk it recursively.

Data
Object store

S3 on the homelab’s Ceph. Chunked, versioned, replicated by someone whose job that is.

Metadata
SQLite

One file on a virtiofs bind from the Mac, compacted at boot, with a 256 MiB page cache.

Litestream
Bucket

Every WAL segment, a few seconds behind, on the same object store. Restored at boot if the file is gone.

01

Network & fleet

1500 on the link, 1280 over the tunnel

The link runs at 1500 so QUIC works; the ranges the Mac forwards over Tailscale are pinned to 1280 per route, and the homelab LAN to 1260. On the other end sit the fleet builder’s amd64 node, the cluster API and the object store — the homelab, one page over.

In the VM
eth0

Link MTU 1500, so QUIC and HTTP/3 work everywhere that is not the tunnel.

Internet · 1500 Tailnet · 1280 Cluster services · 1280 Homelab LAN · 1260
vmnet NAT · Tailscale
macOS

Forwards the tailnet and the homelab LAN into the VM and clamps TCP MSS on the way, which is why only UDP ever noticed the MTU.

The other end
Homelab →

Three sites, two tailnets, one cluster.

Reached from here
Builder · arm64
fleet-local

buildkitd in this VM, over its socket. Never --load: there is no daemon to load into.

Builder · amd64
fleet-cluster

A buildkitd in the cluster, over the tailnet. Each architecture builds natively; nothing is emulated.

kubectl
Cluster API

microk8s, reached over the tailnet. kubectl is pinned within one minor of it.

Push targets
Registries

GHCR for the sites; the in-cluster registry over the service CIDR.