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.