Skip to content

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 plus git, openssh-client, gh, and the claude CLI. 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: pm preview servers, ffmpeg, a headless browser, the Docker CLI (with the buildx and compose plugins), 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 Claude needs to 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.

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 Claude 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:

Terminal window
cd /data/projects/my-analysis
uv venv && . .venv/bin/activate
uv pip install pandas

The 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) together with the buildx and compose CLI plugins — 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.

The plugins are called out because they ship separately from the Docker CLI package, and without them docker compose and docker buildx are both “unknown command” — the cli-plugins directory doesn’t exist at all. They’re pure client-side binaries that talk to whatever socket the deployment chose, so they add no daemon and no privilege of their own.

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:

Terminal window
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:devbox

kubectl 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.

It runs exactly like base — same volume, same auth, same port — just a different tag:

Terminal window
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
  • /data is the one thing you must persist. Everything Paddock keeps — projects, chat transcripts, and its sidecar state — lives there, and HOME=/data so ~/.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_TOKEN for Claude Max/Pro, or ANTHROPIC_API_KEY to use the API (API pricing) — either works on either runtime. Get an OAuth token with claude setup-token on a machine where you’re already logged in.
  • PADDOCK_DANGEROUSLY_ALLOW_OPEN=1 is required for any containerized Paddock, base or devbox: inside a container the app always binds 0.0.0.0 (Docker’s port publishing can’t route to an in-container 127.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:4000 publish 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.

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.

Terminal window
# 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 reserved
pm restart web # restart with a freshly-rebuilt env

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:

VariableDefaultPurpose
PM_PUBLIC_HOSTlocalhostHost 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_MAX5001 / 5999The 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.

The devbox ships the Docker client only (CLI plus the buildx and compose plugins) — 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.sock into the container, so an in-container docker build/run lands 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 projects you trust.
  • Privileged Docker-in-Docker — run a real, isolated daemon inside the container. It never touches the host daemon, but privileged: true weakens 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.