The Dev Box flavor
Paddock ships as two official images built from the same source. Pick the one that matches what your agents actually do:
ghcr.io/edspencer/paddock:latest— the base image. The lean runtime: the Paddock app plusgit,gh, and theclaudeCLI. Everything a stock instance needs to read, write, and reason over text and code — and nothing more.ghcr.io/edspencer/paddock:devbox— the devbox image. Base plus the software-engineering toolbox a coding agent reaches for:pmpreview servers,ffmpeg, a headless browser, the Docker CLI,kubectl, and a scripting kit (python3,pip,uv,jq,rsync).
The devbox only adds tools. It’s the same app, the same data layout, the same
/data volume — so you can stop one profile and start the other against the same
data without losing anything. Reach for devbox when your keepers build and run
apps, not just edit them.
What devbox adds, and why an agent wants each
Section titled “What devbox adds, and why an agent wants each”pm — preview servers on stable ports (PM2)
Section titled “pm — preview servers on stable ports (PM2)”When an agent builds a web app, it needs to actually run it and look at it. pm
is a thin wrapper over PM2 plus a small shared ports
registry. It lets an agent (or you) run long-running dev/preview servers on
stable, assigned ports, with the running state visible to every chat
session — PM2’s daemon and the ports registry are a single shared source of truth
that all callers read. The devbox installs pm to /usr/local/bin/pm and PM2
globally, so the workflow is turnkey. Using pm below has the details.
ffmpeg — media work
Section titled “ffmpeg — media work”Transcoding, extracting frames, trimming audio, building a demo GIF — anything
media-shaped. Agents doing podcast, video, or screenshot-to-clip work need
ffmpeg on PATH; base doesn’t carry it.
The Playwright MCP browser — a real headless Chromium
Section titled “The Playwright MCP browser — a real headless Chromium”The devbox bundles the Playwright MCP
server and a matching headless Chromium, so an agent can drive a real browser:
navigate, click, fill forms, and take screenshots — for example, to QA the very
preview server it just started with pm.
This is on by default in devbox: the image sets PADDOCK_BROWSER_MCP=1, which
tells Paddock to attach the browser MCP tools to keepers at launch. (On base, the
browser tools simply aren’t present.) The browser runs headless and sandboxed by
the container — Paddock launches it --no-sandbox --isolated, because the
container itself is the sandbox.
python3, uv, jq, rsync — the throwaway-script kit
Section titled “python3, uv, jq, rsync — the throwaway-script kit”An agent asked to reshape some JSON or compare two dumps reaches for Python by
habit, whatever the surrounding project is written in. On base that ends in
python3: not found and the script gets rewritten in Node — friction on every
ten-line analysis. devbox carries the interpreter (python3 plus pip and
venv), uv for fast, disposable virtualenvs,
and jq/rsync for the shell-shaped half of the same job.
The line the image draws: interpreters and small CLI utilities in the image,
libraries in the project. So there is a Python here, but no numpy, torch,
or transformers — those are hundreds of megabytes, version-sensitive, and
wrong for any project that pins its own. Install them per project instead:
cd /data/projects/my-analysisuv venv && . .venv/bin/activateuv pip install pandasThe Docker CLI — build and run containers in-container
Section titled “The Docker CLI — build and run containers in-container”Some agent work is itself Docker-shaped: building an image, running a throwaway
container, testing a Compose stack. The devbox ships the Docker client (docker
on PATH) — but no daemon and no privilege baked in. Whether that CLI can
actually reach a daemon is a deployment decision; see
Docker-in-Docker below.
kubectl — making a cluster legible
Section titled “kubectl — making a cluster legible”If your agents ship to Kubernetes, the question they get asked most is some form
of “is the deploy healthy?” — and answering it means kubectl get,
kubectl describe pod, kubectl logs, kubectl rollout status. Without the
client on PATH there is no amount of credentials that lets an agent answer;
devbox carries it.
Same shape as the Docker CLI above: the client only. The image bakes in no
kubeconfig and no cluster credentials — those are per-deployment and yours to
supply. Mount or project one in at run time and point KUBECONFIG at it:
docker run -d --name paddock -p 127.0.0.1:4000:4000 \ -e CLAUDE_CODE_OAUTH_TOKEN=… \ -e PADDOCK_DANGEROUSLY_ALLOW_OPEN=1 \ -e KUBECONFIG=/data/.kube/config \ -v paddock-data:/data \ ghcr.io/edspencer/paddock:devboxkubectl is a pinned static binary, not an apt package — the Kubernetes
project publishes no package in Debian’s archive, Docker’s, or GitHub CLI’s, so
installing it from apt would mean adding a new trust root to the image for one
binary. The version lives in the KUBECTL_VERSION build arg in the Dockerfile
alongside a per-arch SHA-256 that is verified at build time, so the image is
reproducible and the binary matches the arch it ships on.
Running the devbox image
Section titled “Running the devbox image”It runs exactly like base — same volume, same auth, same port — just a different tag:
docker run -d --name paddock -p 127.0.0.1:4000:4000 \ -e CLAUDE_CODE_OAUTH_TOKEN=… `# or ANTHROPIC_API_KEY` \ -e PADDOCK_DANGEROUSLY_ALLOW_OPEN=1 `# containers always bind 0.0.0.0` \ -v paddock-data:/data \ --restart unless-stopped \ ghcr.io/edspencer/paddock:devbox/datais the one thing you must persist. Everything Paddock keeps — projects, chat transcripts, and its sidecar state — lives there, andHOME=/dataso~/.claude/projects(session transcripts) survives restarts, which is what makes resume work. Use a named volume or a real disk you back up.- Claude auth comes in at run time, never baked into the image: set
CLAUDE_CODE_OAUTH_TOKENfor Claude Max/Pro (thecliruntime), orANTHROPIC_API_KEYto use the API (thesdkruntime). Get an OAuth token withclaude setup-tokenon a machine where you’re already logged in. PADDOCK_DANGEROUSLY_ALLOW_OPEN=1is required for any containerized Paddock, base or devbox: inside a container the app always binds0.0.0.0(Docker’s port publishing can’t route to an in-container127.0.0.1), and Paddock’s fail-closed guard would otherwise refuse to boot. This is safe only because the-p 127.0.0.1:4000:4000publish keeps the instance host-only. If you ever publish on a routable address, drop this flag and put a real auth mode in front — see Securing Paddock.
Using pm
Section titled “Using pm”pm gives each named project a stable port (default range 5001–5999) and
injects PORT and HOST=0.0.0.0 into the process, so a framework that honours
those binds correctly without hard-coding a port.
# Start a dev server. --cwd is the code dir; everything after -- is the command.pm start web --cwd /data/projects/my-app -- npm run dev
# See what's running (shared across every chat session):pm status# PROJECT PORT STATE URL# web 5001 online http://localhost:5001
# Tail its logs (add --follow to stream):pm logs web
pm stop web # stop, but keep the assigned port reservedpm restart web # restart with a freshly-rebuilt envConfiguring the URLs
Section titled “Configuring the URLs”By default pm status prints http://localhost:<port>. A few knobs (resolved as
real env var → config file → default) tune that; the two you’re most likely to
touch:
| Variable | Default | Purpose |
|---|---|---|
PM_PUBLIC_HOST | localhost | Host shown in the printed preview URLs. Set it to the hostname your instance is actually reachable at, so the URLs are clickable. |
PM_PORT_MIN / PM_PORT_MAX | 5001 / 5999 | The port-assignment range. If you publish preview ports through a proxy, this is the range to route. |
Set them as environment on the container, or in the pm config file
(/etc/paddock-servers/pm.env by default). The full set — including the ports
registry path and the dev-server data-isolation knobs — is in
scripts/README.md
in the Paddock repo.
Docker-in-Docker
Section titled “Docker-in-Docker”The devbox ships the Docker CLI only — no daemon runs in the container, and no privilege is baked into the image. That’s deliberate: how the CLI reaches a daemon is a security trade-off the deployment recipe makes, not the image.
There are two common shapes, and the
docker/ recipe
documents both:
- Docker-outside-of-Docker (socket mount) — mount the host’s
/var/run/docker.sockinto the container, so an in-containerdocker build/runlands on the host daemon. Cheap, no nested daemon — but it gives the container effectively root-level control of the host through that socket, so only do it for keepers you trust. - Privileged Docker-in-Docker — run a real, isolated daemon inside the
container. It never touches the host daemon, but
privileged: trueweakens the container boundary and you run and maintain a second daemon plus its storage. Prefer the socket mount unless you specifically need daemon isolation.
The recipe’s Compose file wires up the socket mount by default and shows how to switch to privileged DinD.
- The
docker/recipe — the Compose file withbaseanddevboxprofiles. - Deploying Paddock — where and how to run an always-on instance.
- Securing Paddock — authentication in front of Paddock (required before anyone but you can reach it).
- A home-lab setup — a full always-on, composed deployment.