Skip to content

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