Skip to content

Persistence & Volumes

Named Docker volumes persist state across container rebuilds. All volumes are isolated per project using ${localWorkspaceFolderBasename} as a prefix — two projects on the same host never share caches or history. Throughout this page, ${project} is shorthand for ${localWorkspaceFolderBasename} (the workspace folder name).

Named volumes

The Seeded column shows which volumes entrypoint.sh pre-populates on first open; the rest start empty.

Volume Path Purpose Seeded
${project}-zsh-history ~/.zsh_history Shell history No
${project}-bash-history ~/.bash_history Shell history No
${project}-terraform-plugins ~/.terraform.d/plugins Terraform providers From /opt/terraform-plugins-seed
${project}-packer-plugins ~/.config/packer/plugins Packer plugins From /opt/packer-plugins-seed
${project}-ansible-collections ~/.ansible/collections Ansible collections From /opt/ansible-collections-seed
${project}-cache ~/.cache pip, mise downloads No
${project}-claude-code ~/.claude Claude Code state and memory No
${project}-podman-storage ~/.local/share/containers Podman images and layers No
${project}-mitm-home ~/.mitmproxy mitmproxy CA key — persists cert across rebuilds No

Seeding

On first open, entrypoint.sh (seed_caches) copies build-time caches into otherwise-empty volumes. Seeding is idempotent: each target is populated only when its directory is empty, so it never overwrites an already-populated volume.

Exactly three volumes seed from /opt/*-seed/ directories baked into the image — Terraform providers, Packer plugins, and Ansible collections — so those caches are available without network access.

The mise runtime directory (~/.local/share/mise) is deliberately not a volume. The Python/Ruby/Go runtimes — and the pip packages and gems installed into them (ansible, chef-cli, …) — are baked into the image; mounting an empty volume there would shadow them and force a firewall-blocked runtime mise install. Leaving it unmounted keeps the baked runtimes visible with no seeding. entrypoint.sh still attempts a non-fatal mise install only when a project .mise.toml pins a version the image never baked — change runtime versions in the image's .mise.toml and rebuild rather than overriding at runtime. See docs/adr/0014-mise-runtime-volume.md.

All remaining volumes (shell history, ~/.cache, Claude Code state, Podman storage, mitmproxy home) start empty and accumulate state as you work.

Bind mounts

These host paths are bind-mounted into the container and reflect live host state (not volumes — they reset when the host path changes):

Host path Container path Purpose
/run/host-services/ssh-auth.sock /run/host-services/ssh-auth.sock 1Password SSH agent (via Colima VM)

Everything else is ephemeral and reset on rebuild.

Container operations (Podman)

The container includes rootless Podman for building images and running containers. No host Docker socket is needed.

# docker is aliased to podman via podman-docker
docker build -t myimage .
docker run --rm alpine echo hello
podman build -t myimage .
podman images

Podman storage is persisted in the ${project}-podman-storage named volume.

devcontainer.json requires "runArgs": ["--privileged"] for mount namespace operations. See ADR 0007 for the security rationale.