Skip to content

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

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

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
Contributor

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
Contributor Author

Did another round of manual testing against Docker 29.4.0 (nerdctl built from this branch) to check parity. Sharing the results.

Whole-rootfs and subpath read the same as Docker

$ 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 alpine cat /mnt/img/etc/alpine-release
WARNING: Image mount is an experimental feature
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
$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=etc alpine cat /mnt/img/alpine-release
WARNING: Image mount is an experimental feature
3.24.1

Writes are rejected on both (read-only):

$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img alpine touch /mnt/img/x
touch: /mnt/img/x: Read-only file system
$ docker run --rm --mount type=image,source=alpine:latest,destination=/mnt/img alpine touch /mnt/img/x
WARNING: Image mount is an experimental feature
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-07-21T08:16:29Z" 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-07-21T08:16:29Z" level=fatal msg="unexpected key 'subpath' in 'subpath=etc'"
$ nerdctl run --rm --mount type=image,source=alpine:latest,destination=/mnt/img,image-subpath=../etc alpine true
time="2026-07-21T08:16:29Z" 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-07-21T08:16:29Z" 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-07-21T08:16:29Z" level=fatal msg="image-subpath \"nope\" does not exist in image \"alpine:latest\""
$ nerdctl run --rm --mount type=bind,source=/tmp,destination=/mnt,image-subpath=etc alpine true
time="2026-07-21T08:16:29Z" 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 "type=image,source=alpine:latest,destination=/mnt/img,subpath=etc" 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 /var/lib/docker/rootfs/overlayfs/b59a45207c4d.../nope: lstat ...: 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/8b71320c4cd1215df46c5a7da54744badbe10192ad2801d95d7a11494d9bb918-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/8b71320c4cd1215df46c5a7da54744badbe10192ad2801d95d7a11494d9bb918-image-mount/etc",
  "options": ["rbind", "ro"]
}

And inside the container:

$ grep ' /mnt/img ' /proc/<pid>/mountinfo
1969 1959 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 inspect imgtest --format '{{json .Config.Labels}}' | tr ',' '\n' | grep image-mount
"nerdctl/image-mount-hostpaths":"[\"/var/lib/nerdctl/image-mounts/8b71320c4cd1215df46c5a7da54744badbe10192ad2801d95d7a11494d9bb918-image-mount\"]"
"nerdctl/image-mount-snapshots":"[\"8b71320c4cd1215df46c5a7da54744badbe10192ad2801d95d7a11494d9bb918-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 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 unless forced. Not a safety issue, just a behavioral difference worth noting.

$ nerdctl rmi busybox:latest   # while a mount-only container runs
Untagged: docker.io/library/busybox:latest@sha256:fd8d9aa63ba2...
Deleted: sha256:66cb17eae60e...
$ nerdctl exec mntonly ls /mnt/bb/bin/busybox
/mnt/bb/bin/busybox

$ docker rmi busybox:latest   # while a mount-only container runs
Error response from daemon: conflict: unable to delete busybox:latest (must be forced) - container bf6231dbb625 is using its referenced image fd8d9aa63ba2

@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

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 b86abb8 to 7ee3b50 Compare July 19, 2026 06:37
@AkihiroSuda

Copy link
Copy Markdown
Member

Is this still a draft?

@mayur-tolexo
mayur-tolexo marked this pull request as ready for review July 21, 2026 08:34
@mayur-tolexo

Copy link
Copy Markdown
Contributor Author

Is this still a draft?

Yeah its ready for review.

@AkihiroSuda AkihiroSuda added this to the v2.4.0 milestone Jul 29, 2026
@jiwahn

jiwahn commented Aug 1, 2026

Copy link
Copy Markdown

I think images used by existing containers as image mounts should not be removed by rmi

Comment on lines +380 to +384
// "." means the whole rootfs (e.g. from "a/.."); that is the no-subpath case,
// not a subdirectory selection, so reject it as a misuse of image-subpath.
if clean == "." {
return "", fmt.Errorf("image-subpath %q must select a subdirectory, not the image rootfs", p)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but docker accepts this.

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.

3 participants