Opt-in Docker-in-Docker worker image + feature-based addon composition #56

Merged
lz merged 12 commits from feat/dind into main 2026-07-11 11:17:20 +02:00
Owner

Closes #32.

Summary

Adds an opt-in Docker-in-Docker (dind) worker image (and a dind + Playwright combo) so agents can spin up disposable Docker stacks — smoke-test a docker compose change, validate an image builds, or stand up a fresh Nexus and drive its UI with Playwright. Also lays a clean feature-dir addon layout and a docker-bake.hcl build so future addons don't explode into 2^N image variants.

Design/plan: docs/superpowers/specs/2026-07-10-dind-worker-image-design.md · docs/superpowers/plans/2026-07-10-dind-worker-image.md.

What's in it

  • dind capability (nexus/src/lib/server/workers/capabilities.ts): label org.nexus.dindHostConfig Privileged: true, Init: true, and an anonymous /var/lib/docker volume. Label-scoped, so privilege applies only to dind images the operator explicitly picks — that selection is the consent (single-operator model; no separate gate). Composes additively with the playwright capability for the combo image. Unit-tested.
  • Feature-dir addons (worker/addons/<name>/): each addon owns its build-time install.sh and any boot drop-in. The existing Playwright install was migrated into this layout. worker/Dockerfile gains dind (FROM base) and dind-playwright (FROM playwright) stages that share one dind install script — no install-logic duplication.
  • Boot drop-in (worker/addons/dind/entrypoint.d/50-dockerd.sh): starts dockerd --storage-driver=overlay2 before tmux takes PID 1. overlay2 (on the ext4-backed anonymous volume, not overlay-on-overlay) is materially faster than the issue's suggested vfs.
  • docker-bake.hcl: curated targets (base, playwright, dind, dind-playwright, webapp) — we build only the tags we offer, not the blind 2^N.
  • UI: dind + dind-playwright in the official image catalog; the picker shows a "runs in --privileged mode" warning when a dind image is selected.
  • Agent guidance: default-CLAUDE.md documents the disposable-stack workflow (docker run -p 3001:3001 … ; notify-preview 3001).

PR preview images

CI now pushes bounded, per-PR image tags you can pull and spawn:

x.lck.sh/lz/agent-nexus-worker:pr-<n>-dind
x.lck.sh/lz/agent-nexus-worker:pr-<n>-dind-playwright0.1.13
x.lck.sh/lz/agent-nexus-worker:pr-<n>            # base
x.lck.sh/lz/agent-nexus-worker:pr-<n>-playwright0.1.13
x.lck.sh/lz/agent-nexus:pr-<n>                   # webapp

The tag is overwritten on each push (bounded per PR; :latest is never moved on branch builds). A pull_request:closed workflow (.forgejo/workflows/pr-image-cleanup.yml) deletes these on close/merge.

⚙️ Operator prerequisites for registry hygiene

  1. PACKAGE_CLEANUP_TOKEN secret — a PAT with write:package scope, so the close-cleanup job can delete tags. Without it the job no-ops (harmless).
  2. Backstop retention rule (the load-bearing bound, works even if the cleanup job never runs) — under lz → Packages → Settings: Remove versions matching pr-.+, Remove versions older than 7 days. latest is always kept by the container registry regardless.
  3. Optional sanity check before relying on cleanup: curl -H "Authorization: token <PAT>" https://x.lck.sh/api/v1/packages/lz?type=container should list the container packages.

Verification

Automated (green): pnpm lint · pnpm typecheck (0 errors) · pnpm test (562 pass) · pnpm build. CI docker buildx bake builds all five targets.

⚠️ Runtime NOT yet verified — this was implemented in a non-Docker worker, so the dind runtime path is unverified. Please run the Task 8 checklist against a pr-<n>-dind image once CI publishes it:

  • Spawn a Docker (dind) workspace → docker version reports client and server; docker run --rm hello-world exits clean.
  • docker info --format '{{.Driver}}'overlay2; build agent-nexus inside dind — succeeds.
  • docker compose up -d + notify-preview 3001 → operator sees/approves the preview and reaches the inner service.
  • Combo: in a Docker + Playwright workspace, both docker version and playwright-cli --help work (both capabilities applied).
  • Remove the workspace → the anonymous /var/lib/docker volume is reaped (container.remove({ v: true })).
  • A plain base workspace is unchanged: HostConfig.Privileged = false, no docker CLI.

Known risks (documented in-code): dind apt install couples to the base's Debian codename (bookworm today); if dockerd fails to boot on the host kernel, it's most likely cgroup-v2 setup — crib the prep from docker:dind's dockerd-entrypoint.sh into the drop-in.

Follow-up (out of scope)

AGENTS.md facts #5/#12 still say node:20-slim, but the base image is already node:22-slim (predates this branch) — worth a doc fix.

Closes #32. ## Summary Adds an opt-in **Docker-in-Docker (dind)** worker image (and a **dind + Playwright** combo) so agents can spin up disposable Docker stacks — smoke-test a `docker compose` change, validate an image builds, or stand up a fresh Nexus and drive its UI with Playwright. Also lays a clean **feature-dir addon** layout and a `docker-bake.hcl` build so future addons don't explode into 2^N image variants. Design/plan: `docs/superpowers/specs/2026-07-10-dind-worker-image-design.md` · `docs/superpowers/plans/2026-07-10-dind-worker-image.md`. ## What's in it - **dind capability** (`nexus/src/lib/server/workers/capabilities.ts`): label `org.nexus.dind` → `HostConfig` `Privileged: true`, `Init: true`, and an anonymous `/var/lib/docker` volume. Label-scoped, so privilege applies **only** to dind images the operator explicitly picks — that selection is the consent (single-operator model; no separate gate). Composes additively with the playwright capability for the combo image. Unit-tested. - **Feature-dir addons** (`worker/addons/<name>/`): each addon owns its build-time `install.sh` and any boot drop-in. The existing Playwright install was migrated into this layout. `worker/Dockerfile` gains `dind` (`FROM base`) and `dind-playwright` (`FROM playwright`) stages that share one dind install script — no install-logic duplication. - **Boot drop-in** (`worker/addons/dind/entrypoint.d/50-dockerd.sh`): starts `dockerd --storage-driver=overlay2` before tmux takes PID 1. overlay2 (on the ext4-backed anonymous volume, *not* overlay-on-overlay) is materially faster than the issue's suggested vfs. - **`docker-bake.hcl`**: curated targets (base, playwright, dind, dind-playwright, webapp) — we build only the tags we offer, not the blind 2^N. - **UI**: dind + dind-playwright in the official image catalog; the picker shows a **"runs in `--privileged` mode"** warning when a dind image is selected. - **Agent guidance**: `default-CLAUDE.md` documents the disposable-stack workflow (`docker run -p 3001:3001 … ; notify-preview 3001`). ## PR preview images CI now pushes bounded, per-PR image tags you can pull and spawn: ``` x.lck.sh/lz/agent-nexus-worker:pr-<n>-dind x.lck.sh/lz/agent-nexus-worker:pr-<n>-dind-playwright0.1.13 x.lck.sh/lz/agent-nexus-worker:pr-<n> # base x.lck.sh/lz/agent-nexus-worker:pr-<n>-playwright0.1.13 x.lck.sh/lz/agent-nexus:pr-<n> # webapp ``` The tag is overwritten on each push (bounded per PR; `:latest` is never moved on branch builds). A `pull_request:closed` workflow (`.forgejo/workflows/pr-image-cleanup.yml`) deletes these on close/merge. ### ⚙️ Operator prerequisites for registry hygiene 1. **`PACKAGE_CLEANUP_TOKEN` secret** — a PAT with `write:package` scope, so the close-cleanup job can delete tags. Without it the job no-ops (harmless). 2. **Backstop retention rule** (the load-bearing bound, works even if the cleanup job never runs) — under **`lz` → Packages → Settings**: *Remove versions matching* `pr-.+`, *Remove versions older than* `7` days. `latest` is always kept by the container registry regardless. 3. Optional sanity check before relying on cleanup: `curl -H "Authorization: token <PAT>" https://x.lck.sh/api/v1/packages/lz?type=container` should list the container packages. ## Verification **Automated (green):** `pnpm lint` · `pnpm typecheck` (0 errors) · `pnpm test` (562 pass) · `pnpm build`. CI `docker buildx bake` builds all five targets. **⚠️ Runtime NOT yet verified** — this was implemented in a non-Docker worker, so the dind runtime path is unverified. Please run the Task 8 checklist against a `pr-<n>-dind` image once CI publishes it: - [ ] Spawn a **Docker (dind)** workspace → `docker version` reports client **and** server; `docker run --rm hello-world` exits clean. - [ ] `docker info --format '{{.Driver}}'` → `overlay2`; build `agent-nexus` inside dind — succeeds. - [ ] `docker compose up -d` + `notify-preview 3001` → operator sees/approves the preview and reaches the inner service. - [ ] **Combo**: in a **Docker + Playwright** workspace, both `docker version` and `playwright-cli --help` work (both capabilities applied). - [ ] Remove the workspace → the anonymous `/var/lib/docker` volume is reaped (`container.remove({ v: true })`). - [ ] A plain base workspace is unchanged: `HostConfig.Privileged` = false, no `docker` CLI. **Known risks (documented in-code):** dind apt install couples to the base's Debian codename (`bookworm` today); if `dockerd` fails to boot on the host kernel, it's most likely cgroup-v2 setup — crib the prep from `docker:dind`'s `dockerd-entrypoint.sh` into the drop-in. ## Follow-up (out of scope) `AGENTS.md` facts #5/#12 still say `node:20-slim`, but the base image is already `node:22-slim` (predates this branch) — worth a doc fix.
lz added 12 commits 2026-07-11 10:25:51 +02:00
Spec for issue #32. Feature-dir addon model (worker/addons/<name>/),
docker-bake.hcl curated tag matrix, dind capability (overlay2 on anon
volume, privileged, Init), dind+playwright combo, anon-volume cleanup,
picker warning, agent guidance.
ci: push bounded :pr-<n> image tags on PRs + delete them on PR close
All checks were successful
ci / images (pull_request) Successful in 1m53s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 7s
ci / nexus (pull_request) Successful in 4m26s
33a2d13ac7
PR builds now push per-PR tags so the operator can pull and smoke-test them;
a pull_request:closed workflow deletes them, backstopped by a pr-.+ package
retention rule (operator-configured).
lz merged commit 86080ff36f into main 2026-07-11 11:17:20 +02:00
lz deleted branch feat/dind 2026-07-11 11:17:25 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!56
No description provided.