Playbook: Provision the windows-dev Docker Worker Node
Status update (2026-08-11): active guidance again. Briefly marked superseded by Pane (#6132); that pivot was reverted the same day after Pane’s headless daemon setup hit an unresolved architectural hang (see #6132’s final comment). This playbook is the real path forward. Go-live is tracked in #6163: the dispatcher-loop CF-Tunnel/
docker runintegration (prerequisite 5) is now implemented — the soak procedure below applies once the remaining on-host prerequisites (1–4) pass; don’t flipenabled=truebefore a clean soak.
Scope: Bring the windows-dev (WSL2) Docker ephemeral-worker node online (#6110 /
ADR-005 §5b). Mirrors the discipline of
provision-bms-3-dispatch-node.md: the capacity row ships
DISABLED; activation (enabled=true) is the last step, by a human, after the live install
and a soak.
Role: the artifacts (image, wrapper, migration, docs) are
dev-coderdeliverables and land via PR. Every step below marked [sys-admin] is a live server/Docker/CF-Tunnel operation on the windows-dev host and must be executed by sys-admin (or the infra-task worker), not inline by a dev-coder session. Any credential mount is a secret-manager prerequisite (ADR-005 §8) — a dev-coder/sys-admin session never writes SOPS.
What ships in the PR (already done by #6110)
| Artifact | Purpose |
|---|---|
docker/windows-dev-worker/Dockerfile + entrypoint.sh | the maintained worker image |
scripts/run-windows-dev-worker.sh | host wrapper: -m/--cpus caps + 137→exit-2 translation |
scripts/spawn-worker.sh (WORKER_ISOLATION=docker branch) | foreground, no-systemd in-container launch |
supabase/migrations/…_windows_dev_docker_worker_capacity_6110.sql | dev_r_server_capacity row (DISABLED) + claude_accounts |
docs/environments/windows-dev.md | hardware + capacity + contract spec |
scripts/queue-dispatcher-loop.py (spawn_worker() docker-run branch) — #6163 | CF-Tunnel ProxyCommand + detached run-windows-dev-worker.sh launch |
Prerequisites (before enabling)
-
[sys-admin] WSL2 Ubuntu healthy on the dev workstation, with Docker Engine (
docker-ce) installed natively inside the WSL2 distro — the standardapt-based Linux install, not Docker Desktop (owner decision on #6110, 2026-08-11). All code lives under the Linux FS (~/projects), never theC:\mount (ADR-005 §5b).Why native Docker Engine, not Docker Desktop. Docker Desktop’s paid-tier requirement (250+ employees or $10M+ revenue) applies only to Docker Desktop itself — the Windows GUI app that manages its own hidden WSL2 distro. Docker Engine (
docker-ce, the daemon) is free and open-source (Apache 2.0) with no company-size restriction. windows-dev is a headless, systemd-enabled Ubuntu instance (SSH + CF Tunnel, no GUI), so a native Engine install is both the correct fit and sidesteps the licensing question entirely — it is not a workaround.Install (once,
[sys-admin]inside the WSL2 distro — Docker’s official Ubuntu instructions):# Docker Engine apt repo + install (docs.docker.com/engine/install/ubuntu) sudo apt-get update && sudo apt-get install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin # systemd-enabled WSL2 distro → the daemon runs as a normal service: sudo systemctl enable --now docker sudo usermod -aG docker "$USER" # re-login so `docker` works without sudo docker run --rm hello-world # verify the daemon is up -
[sys-admin] CF Tunnel
windows-dev-ssh.tunnel.zintegrowana.onlineprovisioned and reachable (same pattern aslap1/wsl1; seeserver-ssh-connectivity.mdandp24-network-admin.md). Extend it to carry thedocker runinvocation ofrun-windows-dev-worker.sh, not just a bare SSH shell (ADR-005 §3). -
[secret-manager] Per-account
claude auth loginon the windows-dev host for each account inclaude_accounts(claude-runner,claude-runner-2,radieu) — its own independent login, never a copied.credentials.json(ADR-002 Option C, banned). Provision the per-account config dir that gets mounted into the container (one dir per account, one account per container). -
[secret-manager] For
secret-manager-role dispatch only: mount the SOPS age key into that role’s container. dev-coder/other roles get no age key mounted at all (ADR-005 §8). -
Dispatcher docker-dispatch path — DONE (#6163).
queue-dispatcher-loop.py’sspawn_worker()now detects awindows-dev/docker-run host (_uses_docker_run_dispatch), wraps SSH in acloudflared access sshProxyCommand for the CF Tunnel hostname (_is_cf_tunnel_host), and detached-launchesrun-windows-dev-worker.shinstead ofspawn-worker.sh. Tested inscripts/tests/test_spawn_windows_dev_dispatch.py. This unblocks activation — the row is still left disabled until the on-host prerequisites 1–4 + the soak below pass.
Build the image [sys-admin, on the windows-dev host, inside WSL2]
cd ~/projects/p24-infra # the Linux-FS checkout, never /mnt/c/...
# Build context is the repo root (the Dockerfile COPYs . into /opt/p24-infra). Add a .dockerignore
# to keep node_modules/.git/secrets out of the context.
docker build \
-f docker/windows-dev-worker/Dockerfile \
--build-arg GIT_SHA="$(git rev-parse --short HEAD)" \
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t p24-windows-dev-worker:latest .Rebuild the image whenever spawn-worker.sh / agent prompts change — treat it as version-pinned
infra. (CI build + checksum verification of the pinned sops/age binaries is a follow-up hardening.)
Smoke test [sys-admin]
# Dry cap check — confirm the wrapper builds the expected docker run args for a light job.
DOCKER_BIN=echo scripts/run-windows-dev-worker.sh 6110 dev-issue light | tr ' ' '\n' | grep -E 'memory|cpus'
# Expect (light tier, weight-tier update 2026-08-11): --memory 2g / --memory-swap 2g / --cpus 1
# Real single-job run against a throwaway test issue (subscription config mounted):
scripts/run-windows-dev-worker.sh <test-issue> dev-issue light radieu/p24-infra 0 dev_r_worker_queue dev-coder
echo "exit=$?" # 0 = ok; 2 = OOM (bump caps / lower concurrency); 5 = subscription; 255 = infraConfirm under a concurrent interactive-use test (run a light worker while actively using the
machine): the foreground session must not visibly degrade (ADR-005 §5b UX requirement). Tune
--cpus / reserved_ram_gb down if it does.
Activate [sys-admin / secret-manager — human, last step]
Only after all prerequisites pass and the dispatcher docker-path is merged:
UPDATE public.dev_r_server_capacity SET enabled = true WHERE server_label = 'windows-dev';Then watch one dispatch cycle: confirm a light job dispatches over the tunnel, the caps hold, and
the container reclaims cleanly (docker ps empty after exit). Update the dev_r_services rows to
status='active'.
Rollback
UPDATE public.dev_r_server_capacity SET enabled = false WHERE server_label = 'windows-dev';Disabling stops all new dispatch immediately; in-flight containers finish or are docker rm -f’d.
No other host is affected — unclaimed rows fall back to bms-4.
Sequencing guard
Do not scale total fleet concurrency (raise max_workers* above 1–2, add heavy weights) until
the bms-3 RAM-weight dispatch bug (#4717/#4728) is resolved — ADR-005 §5a. windows-dev has no
operating history as a worker host, so this applies with extra force here.
Related
- ADR-005 §3/§4/§5b/§8
docs/environments/windows-dev.mdadding-new-worker.md— Registered Workers tableprovision-bms-3-dispatch-node.md— the ships-disabled precedent