Skip to content

fix(up): forward single-file bind mounts to container run#94

Open
csaller wants to merge 3 commits intoMcrich23:mainfrom
csaller:fix/single-file-bind-mounts
Open

fix(up): forward single-file bind mounts to container run#94
csaller wants to merge 3 commits intoMcrich23:mainfrom
csaller:fix/single-file-bind-mounts

Conversation

@csaller
Copy link
Copy Markdown

@csaller csaller commented May 8, 2026

Closes #93.

The bug

configVolume in ComposeUp.swift treated file vs. directory host sources differently: if the host side of a bind mount resolved to a file, the mount was skipped and only a "The 'container' tool does not support direct file mounts" warning was printed. In practice that warning is often masked by the image-fetch/progress output, so users see a container that silently starts with volumes: entries missing.

But Apple container (0.12.3+) does support file-to-file bind mounts. This works today, straight from the CLI:

container run --rm \
  --volume /host/config.yaml:/app/config.yaml:ro \
  myimage

So the gate in container-compose is no longer (and, based on field use, seems to never have been) correct. Common compose idioms like ./config.yaml:/app/config.yaml:ro or ./init.sh:/docker-entrypoint-initdb.d/init.sh have to be worked around by moving the file into a directory that's already mounted.

The fix

  • When the host path exists, forward the bind mount regardless of whether it's a file or directory — both are supported by container --volume.
  • Preserve the optional mode component (:ro, etc.) when rebuilding the --volume argument. Previously the mode was stripped even for directory mounts, so :ro had no effect there either.

Diff is small and localized to the bind-mount branch of configVolume. The named-volume branch is untouched.

Verification

Minimal reproducer with one directory mount and one file mount:

# docker-compose.yml
services:
  repro:
    image: docker.io/library/alpine:3.20
    command: ["sh","-c","ls -la /app; cat /app/hello.txt; cat /app/data/marker.txt"]
    volumes:
      - ./data:/app/data
      - ./hello.txt:/app/hello.txt:ro

Before (0.11.0): container inspect shows only the directory mount; file mount is absent from configuration.mounts; cat /app/hello.txtNo such file or directory.

After this patch:

[
  { "source": ".../data/",       "destination": "/app/data",       "options": [],     "type": {"virtiofs": {}} },
  { "source": ".../hello.txt",   "destination": "/app/hello.txt",  "options": ["ro"], "type": {"virtiofs": {}} }
]

Container logs:

-rw-r--r-- 1 root root 36 May  8 18:38 hello.txt
hello from a single-file bind mount
hello from a directory bind mount

The :ro on the file mount is now propagated (options: ["ro"]), matching compose semantics.

Test status

Ran swift test. The only failures are in the pre-existing dynamic suite ("What goes up must come down - two containers", "Test WordPress with MySQL compose file", "Test compose with complex dependency chain") — all of those use compose YAMLs with only named volumes (wordpress_data, db_data) and hit the known named-volume bootstrap issue tracked in #52. They reproduce on main without this patch; no code path touched by this change is exercised by them.

I didn't add a unit test because configVolume is a private instance method that pulls from several ComposeUp fields (cwd, fileManager, environmentVariables, projectName). Happy to extract a pure helper and add a static test in a follow-up if you'd prefer that shape.

Test plan

  • docker-compose.yml with a single-file bind mount (e.g. ./config.yaml:/app/config.yaml:ro) brings up a container that can read the file.
  • docker-compose.yml with a directory bind mount continues to work.
  • container inspect <name> shows both mounts in configuration.mounts, with options: ["ro"] preserved for :ro entries.
  • Missing host directories are still auto-created (unchanged behavior).

Apple `container` (0.12.3+) accepts `--volume host/file:container/file[:mode]`
for single-file bind mounts, but `configVolume` was branching on
`isDirectory` and silently dropping the mount (with only a warning) when
the host side resolved to a file. Common compose idioms like
`./config.yaml:/app/config.yaml:ro` or
`./init.sh:/docker-entrypoint-initdb.d/init.sh` therefore never reached
the container.

Forward the bind mount for both file and directory sources, and preserve
the optional mode component (e.g. `:ro`) when rebuilding the
`--volume` argument — previously the mode was stripped even for directory
mounts.

Closes Mcrich23#93
@Mcrich23
Copy link
Copy Markdown
Owner

Mcrich23 commented May 8, 2026

Hi @csaller. Thank you for your PR. I plan to look at it early next week when I have some time (if I don't send something, please ping me!)

For the test, go ahead and add it. Using an @testable import should give you access to any private variables and methods for your evaluation.

Extract configVolume's bind-mount logic into a public free function
composeVolumeToRunArgs so it can be tested without spinning up a real
container runtime. Add 7 static tests covering: single-file mount with
:ro mode (the bug case), single-file mount without mode, directory mount,
directory mount with mode, relative path resolution, missing host path
auto-creation, and invalid format.
@csaller
Copy link
Copy Markdown
Author

csaller commented May 8, 2026

Hi @Mcrich23, thanks! I just added the unit test and they are passing locally, also a test build worked exactly as expected with no regressions detected. Lmk if you need everything else from me. Other than that, have a great weekend!

The function only needs to be reachable from the test target via
@testable import ContainerComposeCore — there's no reason to expose it
as part of the public API surface.
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.

volumes: entries that bind-mount a single file are silently dropped

2 participants