Skip to content

feat(mount): support --mount type=image with image-subpath#4993

Draft
mayur-tolexo wants to merge 1 commit into
containerd:mainfrom
mayur-tolexo:feat/mount-type-image-full
Draft

feat(mount): support --mount type=image with image-subpath#4993
mayur-tolexo wants to merge 1 commit into
containerd:mainfrom
mayur-tolexo:feat/mount-type-image-full

Conversation

@mayur-tolexo

@mayur-tolexo mayur-tolexo commented Jun 22, 2026

Copy link
Copy Markdown

What

Implements --mount type=image end-to-end, matching Docker's --mount type=image (moby/moby#48798), including the image-subpath option.

Part of #3867.

This builds on #4990 (the first commit here is that MVP: read-only whole-rootfs image mount). The second commit adds image-subpath, completing the feature.

  • --mount type=image,source=<image>,destination=<path>: mounts the image rootfs read-only at the destination.
  • --mount type=image,source=<image>,destination=<path>,image-subpath=<rel/path>: mounts only a subdirectory of the image rootfs.

Image mounts are read-only, matching Docker (verified against Docker 29.4.0: writes return Read-only file system).

How

  • Parser (pkg/mountutil): accepts image-subpath, normalizes it, and rejects absolute paths, parent traversal (..), and values that resolve to the rootfs itself. image-subpath is only valid for type=image.
  • Whole-rootfs (no subpath): hands the snapshotter's read-only view straight to the OCI runtime, which owns the mount lifecycle. Unchanged from feat(mount): support --mount type=image #4990.
  • Subpath: an OCI overlay mount cannot select a subdirectory, so the read-only view is materialized on a host directory under the data root, the subpath is resolved with securejoin (blocking symlink escapes), and a read-only bind mount of the resolved directory is added. The host materialization path is recorded on a container label and unmounted/removed on nerdctl rm, alongside the snapshot view.

Not included (parity notes vs moby#48798)

  • Read-write image mounts: Docker presents image mounts read-only in practice, so this matches.
  • Daemon-restart layer restoration / image-layer refcounting: daemon-specific. nerdctl is daemonless; the snapshot view is protected from GC via a gc.root label. A subpath mount's host materialization persists across container restart but not across a host reboot.

Test

  • Unit: pkg/mountutil parser cases (subpath normalization, traversal/absolute/root rejection, non-image rejection).
  • Integration: cmd/nerdctl/container/container_run_mount_image_linux_test.go (subpath exposure, multiple subpaths, read-only, error cases).
  • Manual e2e against Docker 29.4.0 — logs posted in a comment below.

@mayur-tolexo

mayur-tolexo commented Jun 22, 2026

Copy link
Copy Markdown
Author

Did a round of manual testing against Docker 29.4.0 to check parity. Sharing the results.

Whole-rootfs and subpath read the same as Docker

$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img alpine cat /mnt/img/etc/alpine-release
3.24.1
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img alpine cat /mnt/img/etc/alpine-release
3.24.1

$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=etc alpine cat /mnt/img/alpine-release
3.24.1
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=etc alpine cat /mnt/img/alpine-release
3.24.1

Writes are rejected on both (read-only):

$ ... touch /mnt/img/x
touch: /mnt/img/x: Read-only file system

Bad mount specs are rejected, same as Docker

nerdctl:

$ nerdctl run --rm --mount type=image,destination=/mnt/img alpine true
time="2026-06-22T16:54:52Z" level=fatal msg="type=image requires a source (the image reference)"
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,subpath=etc alpine true
time="2026-06-22T16:54:52Z" level=fatal msg="mount option \"subpath\" is not yet supported"
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=../etc alpine true
time="2026-06-22T16:54:52Z" level=fatal msg="image-subpath \"../etc\" escapes the image rootfs"
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=/etc alpine true
time="2026-06-22T16:54:52Z" level=fatal msg="image-subpath must be relative to the image rootfs, got \"/etc\""
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=nope alpine true
time="2026-06-22T16:54:52Z" level=fatal msg="image-subpath \"nope\" does not exist in image \"docker.io/library/alpine:latest\""
$ nerdctl run --rm --mount type=bind,source=/tmp,destination=/mnt,image-subpath=etc alpine true
time="2026-06-22T16:54:52Z" level=fatal msg="image-subpath is only supported for type=image"

Docker on the same specs:

$ docker run --rm --mount type=image,destination=/mnt/img alpine true
docker: Error response from daemon: invalid mount config for type "image": field Source must not be empty
$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,subpath=etc alpine true
invalid argument "..." for "--mount" flag: unknown option 'subpath' in 'subpath=etc'
$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=../etc alpine true
docker: Error response from daemon: invalid mount config for type "image": subpath must be a relative path within the volume
$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=/etc alpine true
docker: Error response from daemon: invalid mount config for type "image": subpath must be a relative path within the volume
$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=nope alpine true
docker: Error response from daemon: cannot access path .../nope: ... no such file or directory
$ docker run --rm --mount type=bind,source=/tmp,destination=/mnt,image-subpath=etc alpine true
invalid argument "..." for "--mount" flag: cannot mix 'image-*' options with mount type 'bind'

How the subpath mount is wired

nerdctl is daemonless, so for a subpath it materializes the image rootfs on the host (read-only) and bind-mounts the subdir into the container. For a container started with image-subpath=etc:

$ grep image-mounts /proc/mounts
/dev/vda1 /var/lib/nerdctl/image-mounts/6dca0942365322a1d73059ab7945080b4c9cb470e7f1665e0f1d714804f87f9c-image-mount ext4 ro,relatime,discard 0 0

The OCI mount injected for /mnt/img (from the runtime config.json):

{
  "destination": "/mnt/img",
  "type": "bind",
  "source": "/var/lib/nerdctl/image-mounts/6dca0942365322a1d73059ab7945080b4c9cb470e7f1665e0f1d714804f87f9c-image-mount/etc",
  "options": ["rbind", "ro"]
}

And inside the container:

$ grep ' /mnt/img ' /proc/self/mountinfo
117 110 254:1 /docker/volumes/nerdctl-cd-data/_data/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs/etc /mnt/img ro,relatime master:1 - ext4 /dev/vda1 rw,discard

The snapshot view and host path are recorded on the container so they can be cleaned up on removal:

nerdctl/image-mount-snapshots = ["6dca0942365322a1d73059ab7945080b4c9cb470e7f1665e0f1d714804f87f9c-image-mount"]
nerdctl/image-mount-hostpaths = ["/var/lib/nerdctl/image-mounts/6dca0942365322a1d73059ab7945080b4c9cb470e7f1665e0f1d714804f87f9c-image-mount"]

On nerdctl rm both are torn down. With two subpath mounts:

$ grep -c image-mounts /proc/mounts   # while running
2
$ grep -c image-mounts /proc/mounts   # after stop + rm
0

One difference from Docker

rmi of an image that's used only as a mount source (not the container's base image) succeeds in nerdctl, and the running mount keeps working because the snapshot view is gc-rooted. Docker refuses with "must be forced". Not a safety issue, just a behavioral difference worth noting.

Mount an image's filesystem into a container read-only, matching Docker:
--mount type=image,source=<image>,destination=<path>. The source image is
ensured and unpacked, a read-only snapshot view of its rootfs is created and
mounted at the destination, and the view is removed when the container is
deleted.

The image-subpath option exposes a single directory of the image rootfs at
the destination instead of the whole rootfs. An OCI overlay mount cannot
select a subdirectory, so a subpath mount materializes the read-only view on
a host directory under the data root, resolves the subpath with securejoin
(blocking absolute paths and parent traversal, including via symlinks), and
bind-mounts the resolved directory read-only into the container. The host
materialization path is recorded on a container label and unmounted and
removed on container deletion, alongside the snapshot view.

The whole-rootfs path hands the snapshotter mount straight to the runtime,
which owns its lifecycle.

Mounting the same image at multiple destinations is supported; the
corresponding tests are skipped on Docker, which rejects mounting the same
image more than once.

Signed-off-by: Mayur Das <mayur.das@neevcloud.com>
@mayur-tolexo mayur-tolexo force-pushed the feat/mount-type-image-full branch from 18a55fe to 870f56c Compare June 22, 2026 17:13
@AkihiroSuda

Copy link
Copy Markdown
Member

This builds on #4990 (the first commit here is that MVP: read-only whole-rootfs image mount). The second commit adds image-subpath, completing the feature.

I only see a single commit

@AkihiroSuda

Copy link
Copy Markdown
Member

If this PR is expected to be merged after #4990, please click Convert to draft

@mayur-tolexo mayur-tolexo marked this pull request as draft June 22, 2026 18:28
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.

2 participants