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 run integration (prerequisite 5) is now implemented — the soak procedure below applies once the remaining on-host prerequisites (1–4) pass; don’t flip enabled=true before 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-coder deliverables 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)

ArtifactPurpose
docker/windows-dev-worker/Dockerfile + entrypoint.shthe maintained worker image
scripts/run-windows-dev-worker.shhost 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.sqldev_r_server_capacity row (DISABLED) + claude_accounts
docs/environments/windows-dev.mdhardware + capacity + contract spec
scripts/queue-dispatcher-loop.py (spawn_worker() docker-run branch) — #6163CF-Tunnel ProxyCommand + detached run-windows-dev-worker.sh launch

Prerequisites (before enabling)

  1. [sys-admin] WSL2 Ubuntu healthy on the dev workstation, with Docker Engine (docker-ce) installed natively inside the WSL2 distro — the standard apt-based Linux install, not Docker Desktop (owner decision on #6110, 2026-08-11). All code lives under the Linux FS (~/projects), never the C:\ 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
  2. [sys-admin] CF Tunnel windows-dev-ssh.tunnel.zintegrowana.online provisioned and reachable (same pattern as lap1/wsl1; see server-ssh-connectivity.md and p24-network-admin.md). Extend it to carry the docker run invocation of run-windows-dev-worker.sh, not just a bare SSH shell (ADR-005 §3).

  3. [secret-manager] Per-account claude auth login on the windows-dev host for each account in claude_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).

  4. [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).

  5. Dispatcher docker-dispatch path — DONE (#6163). queue-dispatcher-loop.py’s spawn_worker() now detects a windows-dev/docker-run host (_uses_docker_run_dispatch), wraps SSH in a cloudflared access ssh ProxyCommand for the CF Tunnel hostname (_is_cf_tunnel_host), and detached-launches run-windows-dev-worker.sh instead of spawn-worker.sh. Tested in scripts/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 = infra

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