Summary
Enable gastown agents to run wrangler dev with Cloudflare Containers support inside the gastown container itself. This allows gastown to debug, develop, and test its own Worker + Container stack — a polecat can spin up a local wrangler dev environment, make changes to the gastown Worker/Container code, and validate them before pushing.
Use Case
A gastown convoy working on gastown itself needs to:
- Clone the cloud repo (already works)
- Run
wrangler dev for cloudflare-gastown (needs Docker for Containers)
- The dev Worker spins up a local container via Docker
- Run integration tests or manual validation against the local dev environment
- Push the changes
Step 2 currently fails because the gastown container has no Docker daemon. wrangler dev detects the containers config in wrangler.jsonc and tries to use Docker to build/run the dev container image.
Architecture
Cloudflare Containers support rootless Docker-in-Docker (docs). Gastown uses raw Containers (extends Container<Env>), NOT the Sandbox SDK, so we have more control than the DinD guide assumes — no /sandbox entrypoint requirement, no @cloudflare/sandbox SDK constraints.
Cloudflare Container VM (gastown production container)
├── bun run src/main.ts (gastown control server — existing)
├── dockerd --rootless (background process)
│ └── Docker socket at $XDG_RUNTIME_DIR/docker.sock
└── Agent runs `wrangler dev --env dev`
└── wrangler builds + runs inner container via Docker
└── Inner container (gastown dev container from Dockerfile.dev)
What we need in the gastown container image
- Rootless Docker — the
dockerd-rootless binary + dependencies
- Docker CLI —
docker command for wrangler to call
- A boot script that starts
dockerd in the background on container startup (before the control server)
DOCKER_HOST env var pointing to the rootless Docker socket
- Wrangler — already available via
npx wrangler (Node.js is installed)
What we do NOT need
- The Sandbox SDK (
@cloudflare/sandbox) — we manage Docker ourselves
- The
/sandbox entrypoint binary — we use our own bun run src/main.ts
- Privileged mode — rootless Docker works without it
Dockerfile Changes
Two approaches:
Option A: Install rootless Docker into the existing Debian image (recommended)
FROM oven/bun:1-slim
# ... existing apt-get installs (git, node, gh, ripgrep, build-essential, etc.)
# Install rootless Docker
RUN apt-get update && \
apt-get install -y --no-install-recommends \
uidmap \ # for rootless user namespaces
dbus-user-session \
fuse-overlayfs \ # for rootless overlay storage
slirp4netns \ # for rootless networking
&& curl -fsSL https://get.docker.com/rootless | sh \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Set Docker environment for the agent user
ENV DOCKER_HOST=unix:///run/user/1000/docker.sock
ENV XDG_RUNTIME_DIR=/run/user/1000
Pro: Extends the existing image with minimal changes. All existing tooling (bun, node, git, kilo) remains as-is.
Con: Larger image. Rootless Docker on Debian requires more setup than Alpine-based docker:dind-rootless.
Option B: Multi-stage — copy Docker from dind-rootless
FROM docker:dind-rootless AS docker-src
FROM oven/bun:1-slim
# Copy Docker binaries from the dind-rootless image
COPY --from=docker-src /usr/local/bin/docker /usr/local/bin/docker
COPY --from=docker-src /usr/local/bin/dockerd /usr/local/bin/dockerd
COPY --from=docker-src /usr/local/bin/dockerd-rootless.sh /usr/local/bin/dockerd-rootless.sh
COPY --from=docker-src /usr/local/bin/rootlesskit /usr/local/bin/rootlesskit
COPY --from=docker-src /usr/local/bin/containerd* /usr/local/bin/
COPY --from=docker-src /usr/local/bin/runc /usr/local/bin/runc
# Install runtime deps for rootless Docker
RUN apt-get update && \
apt-get install -y --no-install-recommends uidmap fuse-overlayfs slirp4netns && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# ... rest of existing Dockerfile
Pro: Copies pre-built binaries, avoids the get.docker.com install script.
Con: More fragile (binary compatibility between Alpine-built and Debian).
Recommendation: Option A — simpler, more maintainable.
Boot Sequence
The container entrypoint needs to start dockerd before the control server:
#!/bin/bash
set -eu
# Start rootless dockerd in the background
export XDG_RUNTIME_DIR=/run/user/$(id -u)
mkdir -p $XDG_RUNTIME_DIR
dockerd-rootless.sh --iptables=false --ip6tables=false &
# Wait for Docker readiness
until docker version >/dev/null 2>&1; do sleep 0.2; done
echo "Docker is ready"
# Start the gastown control server (existing entrypoint)
exec bun run src/main.ts
Replace the Dockerfile CMD:
COPY boot.sh /app/boot.sh
CMD ["/app/boot.sh"]
Cloudflare Container Constraints
| Constraint |
Impact |
Mitigation |
| No iptables |
Inner containers can't have isolated networks |
Use --network=host on all docker run / docker build commands |
| Rootless only |
Can't use privileged features |
Rootless Docker is sufficient for wrangler dev |
| Ephemeral storage |
Docker images lost on container sleep/eviction |
Agents should docker pull / docker build as needed. Consider using a registry cache. |
| linux/amd64 only (production) |
Dev Dockerfile.dev uses arm64 for Apple Silicon |
The inner container must build for amd64. Update Dockerfile.dev to support amd64 or add --platform linux/amd64 |
Wrangler Configuration
When an agent runs wrangler dev inside the container, it needs:
# Set Docker socket for wrangler to discover
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
# Or configure in wrangler.jsonc:
# "dev": { "container_engine": "$XDG_RUNTIME_DIR/docker.sock" }
# Run wrangler dev
npx wrangler dev --env dev --ip 0.0.0.0
The --network=host constraint means the inner container's exposed ports are available on the outer container's network. The agent can access the dev Worker at http://localhost:8787 and the dev container at http://localhost:8080.
Conditional Docker (Don't penalize normal agents)
Docker adds ~200-300MB to the image and the dockerd boot process adds 2-3 seconds to cold start. Most towns don't need Docker — only towns working on gastown itself or other containerized projects.
Options:
- Always install Docker — simplest. Accept the image size increase. Gate
dockerd startup on a config flag so it doesn't run unless needed.
- Separate "docker-enabled" image — a second Dockerfile (
Dockerfile.docker) that extends the base with Docker. Towns that need DinD are configured to use this image.
- Install on demand — the agent runs a setup script that installs Docker at runtime. Slow (~30s) but zero image size impact for normal agents.
Recommendation: Option 1 with a gated boot. Install Docker in the image (it's just binaries). Only start dockerd if a flag is set (e.g., ENABLE_DOCKER=1 env var from town config). Normal agents pay the image size cost but not the boot time cost.
# In boot.sh:
if [ "$ENABLE_DOCKER" = "1" ]; then
dockerd-rootless.sh --iptables=false --ip6tables=false &
until docker version >/dev/null 2>&1; do sleep 0.2; done
echo "Docker is ready"
fi
exec bun run src/main.ts
Image Size Impact
| Component |
Size |
| Current image (base + tools) |
~350MB |
| Rootless Docker binaries |
~200MB |
| Runtime deps (uidmap, fuse-overlayfs, slirp4netns) |
~10MB |
| Total with Docker |
~560MB |
Combined with #1976 (dev tools expansion, +250MB), the full image would be ~810MB. This is within reason for a dev container — GitHub Codespaces images are 5-10GB.
Acceptance Criteria
References
Summary
Enable gastown agents to run
wrangler devwith Cloudflare Containers support inside the gastown container itself. This allows gastown to debug, develop, and test its own Worker + Container stack — a polecat can spin up a local wrangler dev environment, make changes to the gastown Worker/Container code, and validate them before pushing.Use Case
A gastown convoy working on gastown itself needs to:
wrangler devfor cloudflare-gastown (needs Docker for Containers)Step 2 currently fails because the gastown container has no Docker daemon.
wrangler devdetects thecontainersconfig inwrangler.jsoncand tries to use Docker to build/run the dev container image.Architecture
Cloudflare Containers support rootless Docker-in-Docker (docs). Gastown uses raw Containers (
extends Container<Env>), NOT the Sandbox SDK, so we have more control than the DinD guide assumes — no/sandboxentrypoint requirement, no@cloudflare/sandboxSDK constraints.What we need in the gastown container image
dockerd-rootlessbinary + dependenciesdockercommand for wrangler to calldockerdin the background on container startup (before the control server)DOCKER_HOSTenv var pointing to the rootless Docker socketnpx wrangler(Node.js is installed)What we do NOT need
@cloudflare/sandbox) — we manage Docker ourselves/sandboxentrypoint binary — we use our ownbun run src/main.tsDockerfile Changes
Two approaches:
Option A: Install rootless Docker into the existing Debian image (recommended)
Pro: Extends the existing image with minimal changes. All existing tooling (bun, node, git, kilo) remains as-is.
Con: Larger image. Rootless Docker on Debian requires more setup than Alpine-based
docker:dind-rootless.Option B: Multi-stage — copy Docker from dind-rootless
Pro: Copies pre-built binaries, avoids the
get.docker.cominstall script.Con: More fragile (binary compatibility between Alpine-built and Debian).
Recommendation: Option A — simpler, more maintainable.
Boot Sequence
The container entrypoint needs to start dockerd before the control server:
Replace the Dockerfile CMD:
Cloudflare Container Constraints
--network=hoston alldocker run/docker buildcommandsdocker pull/docker buildas needed. Consider using a registry cache.Dockerfile.devto support amd64 or add--platform linux/amd64Wrangler Configuration
When an agent runs
wrangler devinside the container, it needs:The
--network=hostconstraint means the inner container's exposed ports are available on the outer container's network. The agent can access the dev Worker athttp://localhost:8787and the dev container athttp://localhost:8080.Conditional Docker (Don't penalize normal agents)
Docker adds ~200-300MB to the image and the
dockerdboot process adds 2-3 seconds to cold start. Most towns don't need Docker — only towns working on gastown itself or other containerized projects.Options:
dockerdstartup on a config flag so it doesn't run unless needed.Dockerfile.docker) that extends the base with Docker. Towns that need DinD are configured to use this image.Recommendation: Option 1 with a gated boot. Install Docker in the image (it's just binaries). Only start
dockerdif a flag is set (e.g.,ENABLE_DOCKER=1env var from town config). Normal agents pay the image size cost but not the boot time cost.Image Size Impact
Combined with #1976 (dev tools expansion, +250MB), the full image would be ~810MB. This is within reason for a dev container — GitHub Codespaces images are 5-10GB.
Acceptance Criteria
dockerdstarts conditionally viaENABLE_DOCKERflagDOCKER_HOSTenv var set to rootless socket pathdocker buildanddocker runinside the containerwrangler devwith container support (builds + runs inner container)--network=host(enforced by agent prompt or wrapper script)References
cloudflare-gastown/wrangler.jsonc:122-130— dev container configcloudflare-gastown/container/Dockerfile.dev— current dev container image