Skip to content

0014. Do not volume-mount the mise runtime directory

Status

Accepted (amends ADR 0002)

Context

Runtimes (Python, Ruby, Go) are managed by mise and baked into the image at build time (Dockerfile Layer D), where the network is unrestricted. See ADR 0002. Everything installed into those runtimes is baked too: the Layer E pip packages (ansible, ansible-lint, pre-commit, …) and the Layer F gems (chef-cli, cookstyle, …) live under ~/.local/share/mise/installs.

devcontainer.json used to mount a persistent named volume, ${project}-mise-data, over ~/.local/share/mise. A fresh named volume starts empty and overlays the image directory, so on first start it shadowed every baked runtime and the pip/gem toolchain installed into them.

entrypoint.sh (seed_caches) responded to the empty directory by running a fatal mise install, which tried to re-download the identical baked versions from the network — while the proxy/firewall were not yet up during postCreateCommand, and against hosts (e.g. dl.google.com for Go) that the egress allowlist deliberately excludes. The result: tunnel error: Connection refused and an aborted setup. On arm64/musl the Ruby half was doubly doomed (no prebuilt binary; ruby-build scripts were themselves shadowed by the empty ~/.cache volume).

At runtime the container enforces a strict egress policy, so re-downloading a runtime cannot work regardless.

Decision

Do not mount a named volume over ~/.local/share/mise. Remove the ${project}-mise-data volume from devcontainer.json (this repo's own and the examples/ template).

  • The baked runtimes and the pip/gem toolchain installed into them are always visible; nothing shadows them.
  • entrypoint.sh no longer seeds the directory. mise install runs only if mise ls --missing reports a genuinely missing tool (a project .mise.toml pinning a version the image never baked), and any failure is non-fatal.
  • Runtime version overrides are unsupported. The egress policy blocks the downloads a different version would need. To change a runtime version, bump the image's .mise.toml (Renovate tracks it) and rebuild. examples/.mise.toml documents this and no longer advertises override examples.

Consequences

Accepted: - First-run setup can no longer break on the mise step; the default case does zero network I/O for runtimes. - No image growth. (An earlier iteration seeded an empty volume from a build-time /opt/mise-seed snapshot; that cp -a duplicated the ~1.1 GB runtime tree, roughly doubling the largest part of the image and exhausting the CI runner's disk during the build. Removing the volume removes the need for a seed at all.) - ~/.local/share/mise is no longer persisted across container recreations. This loses nothing of value: the runtimes are baked (recreated identical on every start), and runtime-written state there could not usefully persist because runtime installs are blocked by the egress policy.

Rejected alternatives: - Seed the empty volume from a build-time /opt/mise-seed copy. Works, but duplicates ~1.1 GB into the image (slower pulls for every consumer) and broke the CI build on no space left on device. Rejected in favor of removing the volume entirely. - Allowlist dl.google.com et al. and repair the ordering. Weakens the egress posture and would still fail because the proxy isn't up during postCreateCommand.

Migration note

Existing consuming projects: after update-project.sh syncs the new devcontainer.json, the ${project}-mise-data volume is no longer mounted (the old Docker volume, if any, is left orphaned and can be removed with docker volume rm). No action is required for correctness.