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, and the Docker CLI.
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.
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) — 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.
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.