0006. Reusable Image Architecture¶
Status¶
Accepted
Context¶
The dev container was originally self-contained: .devcontainer/devcontainer.json
built from ../Dockerfile and everything lived in one repo. As more
infrastructure projects need the same tooling (Terraform, Ansible, Chef, OCI CLI,
etc.), each project would either:
- Duplicate the Dockerfile (maintenance burden, version drift).
- Git submodule this repo (fragile, slow, confusing UX).
- Build from source each time (slow first-open for every project).
None of these scale. The container should be a pre-built image that projects
reference with a thin devcontainer.json.
Decision¶
Split the container into two usage modes:
Build mode (this repo)¶
The dev-container repo builds, tests, and optionally publishes the
image. Its .devcontainer/devcontainer.json builds from ../Dockerfile for
developing the container itself. A Makefile provides build, test, tag,
and push targets.
Consume mode (other projects)¶
Other projects carry only a .devcontainer/devcontainer.json that references
"image": "dev-container:latest". Template files in examples/ provide
starting points.
Parameterized entrypoint¶
entrypoint.sh uses WORKSPACE_DIR (default: /workspaces) instead of
hardcoded paths. Consuming projects set WORKSPACE_DIR=${containerWorkspaceFolder}
in their devcontainer.json remoteEnv.
Volume isolation strategy¶
All named volumes are isolated per project. Volume names are prefixed with
${localWorkspaceFolderBasename} so each project gets its own caches, runtimes,
shell history, and Claude Code state. The seed mechanism in entrypoint.sh
copies pre-built caches from /opt/*-seed/ into empty volumes on first run,
so isolation does not require network access.
Egress allowlist extension¶
firewall.sh supports EGRESS_ALLOWLIST_EXTRA for project-specific endpoints.
The base allowlist is baked into the image; projects extend it without modifying
the image.
Consequences¶
Easier: - Single place to update tooling; all projects get updates by rebuilding. - Fast container startup for consuming projects (no build step, just pull/start). - Consistent environment across all infrastructure projects. - Each project gets a clean, isolated environment with no cross-project contamination.
Harder:
- Must remember to rebuild the image after Dockerfile changes (make build).
- Consuming projects cannot customize baked-in tools (only runtimes via mise
and egress via allowlist extension).
- Per-project volumes use more disk and require seeding on first open (mitigated
by the image-baked seed mechanism which runs without network access).