Skip to content

fix(compose): carry volume and tmpfs mounts onto the primary service - #143

Merged
bilby91 merged 1 commit into
mainfrom
fix/compose-volume-tmpfs-mounts
Oct 27, 2026
Merged

bilby91 merged 1 commit into
mainfrom
fix/compose-volume-tmpfs-mounts

Conversation

@bilby91

@bilby91 bilby91 commented Oct 27, 2026

Copy link
Copy Markdown
Member

Problem

For compose-source devcontainers, composeBindMounts kept only bind mounts from ResolvedConfig.Mounts. Volume and tmpfs mounts from devcontainer.json and from feature/image metadata were dropped. The function's comment said other types were "silently dropped". ApplyRunOverride and WriteRunOverride could only add binds.

docker-in-docker declares {"source":"dind-var-lib-docker-${devcontainerId}","target":"/var/lib/docker","type":"volume"}. Without that mount, the inner dockerd's data root stays on the container's overlay root, and docker run fails because overlay cannot be mounted on overlay. The image path (buildMounts) already passes every mount type through. This is the same class of bug as #103 (R1, "resolved but never applied"), but for mounts.

Fix

This adds compose.Mount and compose.Override.ExtraMounts. ExtraBindMounts is unchanged. Both backends are covered (R2):

  • native: ApplyRunOverride appends volume and tmpfs service volumes. It also declares each named volume on project.Volumes, so ensureNamedVolumes creates it as <project>_<name> with the project labels.
  • shell-out: WriteRunOverride emits the same service volumes, plus a top-level volumes: entry for each named volume. serializeServiceVolume now omits an empty source and carries nocopy and the tmpfs size/mode.
  • Engine.Up fills ExtraMounts from cfg.Mounts and from non-bind UpOptions.ExtraMounts. This is the same set buildRunSpec uses. ${devcontainerId} is already resolved by applyMetadataMerge.
  • Target conflicts: if the user's compose service already mounts something at the same target, the user's entry is kept and the metadata mount is skipped. This matches the existing workspace-bind dedup. The reference CLI's override would replace the user's entry instead (R5); the CHANGELOG notes this divergence.
  • Existing top-level volumes: a top-level volume the user already declares under the same name is left exactly as declared.
  • Invalid types: any other mount type in ExtraMounts is rejected with an error.

R3, recreate: a workspace whose features declare such a mount gets a new service-volume entry, so its config hash drifts on the first Up after upgrading. Without AdoptExisting, the primary container is recreated. That is the same drift the per-boot feature image rebuild already causes, and the container being recreated is the one that never had the mount.

The CHANGELOG cuts 0.5.1. The change only adds API, which follows the v0.4.3 precedent for additive patch releases.

Tests

  • compose/override_mounts_test.go covers both backends:

    • a feature type: volume mount produces a service volume and a project volume;
    • tmpfs passes through with its options;
    • volume readonly/nocopy and anonymous volumes;
    • binds are unchanged;
    • a user's mount at the same target wins;
    • a user's top-level declaration is kept;
    • invalid types are rejected.

    The native round trip goes through Orchestrator.Up and serviceToRunSpec: the RunSpec gets dc-x_dind-…, and CreateVolume receives the project labels. The shell-out tests load user.yml plus the generated override through compose-go, which is the same merge docker compose does.

  • up_compose_mounts_test.go covers the engine level: composeMounts/composeBindMounts built from merged feature metadata, plus upComposeNative and upComposeShellout with fakes.

  • test/integration/compose_dind_test.go is a compose devcontainer with ghcr.io/devcontainers/features/docker-in-docker:2 on both backends. It checks the project volume is mounted at /var/lib/docker and runs docker run --rm hello-world inside.

Local results (Docker 29.1.3, Compose 2.40.3):

  • go test -race ./... passes on Go 1.25.14 and on 1.27.3.
  • go vet passes, including with -tags=integration.
  • golangci-lint v2.5.0 reports 0 issues.
  • The new integration test passes on both backends.
  • On main without the fix, the same test fails on both backends: there is no /var/lib/docker mount, and docker run exits 125 with failed to mount … overlay.

🤖 Generated with Claude Code

https://claude.ai/code/session_0172W3jer4F1sxWk3US29Muz

composeBindMounts kept only bind mounts from ResolvedConfig.Mounts, so
volume and tmpfs mounts from devcontainer.json and feature/image
metadata never reached a compose-source devcontainer. docker-in-docker
declares dind-var-lib-docker-${devcontainerId} -> /var/lib/docker as a
volume; without it the inner dockerd's data root sits on the
container's overlay root and every `docker run` fails with EINVAL
(overlay on overlay). The image path already passed every type through.

Add compose.Mount and Override.ExtraMounts and apply them on both
backends:

- native: ApplyRunOverride appends volume/tmpfs service volumes and
  declares each named volume on project.Volumes so ensureNamedVolumes
  creates it as <project>_<name> with the project labels;
- shell-out: WriteRunOverride emits the same service volumes and a
  top-level volumes: entry for each named volume.

A target the user's compose service already mounts keeps the user's
entry; a top-level volume the user already declares is left as is.
Binds (ExtraBindMounts) are unchanged. ${devcontainerId} is already
substituted by applyMetadataMerge, as on the image path.

Cut 0.5.1 in the changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172W3jer4F1sxWk3US29Muz
@bilby91

bilby91 commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@dap-code-review-by-crunchloop review

The first review run for c949405 did not complete (check: "Review did not complete"). Requesting another round.

@dap-code-review-by-crunchloop

Copy link
Copy Markdown

On it. A review of the current head is on its way.

@bilby91
bilby91 merged commit 975e5e1 into main Oct 27, 2026
9 of 10 checks passed
@bilby91
bilby91 deleted the fix/compose-volume-tmpfs-mounts branch October 27, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant