Repository: moby/moby (language: go)

Issue:
Proposal: mount from image
There are many cases where it is useful to mount an immutable data volume into a container, with configuration or static data (or code, it could be an apt repo or whatever).

I am proposing we add a new mount type so you can do (or whatever `--mount` is renamed to) - it would also be supported in compose files/bundles.

```docker service create --mount type=image,src=alpine:3.5,dest=/alpine ...```

These would fetch the image from the repository if necessary, allowing tags or hashes to be specified, and mount it at the specified mountpoint. This would always be read only. Docker would unpack the image and mount it, presumably using layers but this would be an implementation detail, as being read only this would not be visible.

Use cases include configuration data (not secrets, but miscellaneous scripts and config), actual data, apt repos, npm repos etc. If a hash was used it would be guaranteed to be consistent across multiple tasks in a service.

cc @AkihiroSuda @tonistiigi @stevvooe 


Discussion / hints:
Slightly related; https://github.com/docker/docker/issues/16079. Wondering; would there be use-cases to have a writable layer on top of that mounted image? Would that (technically) be possible?
This is huge!

> Wondering; would there be use-cases to have a writable layer on top of that mounted image? Would that (technically) be possible?

Baby steps, please.

It would also be excellent if you could pluck from paths in the image:

```
docker service create --mount type=image,src=alpine:3.5/theconfig.cfg,dest=/etc/myapp.cfg ...
```

The syntax above is bogus, but this is the killer feature.
@thaJeztah a writeable layer would be possible as a future change, but I think there are many advantages to starting with read only.

@stevvooe adding paths should be easy, as it is just a different bind from the same image. Probably would want `path=...` so less microformatty.

Thanks, just getting future options clear, absolutely no need to have r/w initially
> @stevvooe adding paths should be easy, as it is just a different bind from the same image. Probably would want path=... so less microformatty.

Agreed. A `path` parameter would make that a whole lot less awful.
In case it's of interest, I submitted a proof of concept patch yesterday on another issue that does kind of similar. Instead of mounting into a container, it would allow a Dockerfile to kick off an independent build, and then snarf the final layer it created back into the current image build. Our use case is more around pulling a bunch of git code in, though the demo I put in the issue uses it as a build system.

See: https://github.com/docker/docker/issues/14704#issuecomment-276048577
@aeijdenberg That is a very different use case that being able to mount an image. Mounting an image allows you to effectively have data images. This is very interesting for deploying projects that require a common app server, such as java, php, fastcgi or python (wsgi). It is also very interesting for distributing common configuration throughout a cluster of machines, which has been a some what of a pain point. Combined with something like `PIVOT`, you could also construct build harnesses.

If you are interested in a mounting a git repo, I do have a POC volume driver that could be whipped into shape, but I am not sure I have the bandwidth to support something like that. Please let me know.
Hi @stevvooe , thanks for your reply. Yes, it likely is a bit of a different case - but since this issue came up a few times in my searches when I was searching for prior work, I figured there were enough keywords to make it worth putting a link in.

What we're looking to do is slightly more complicated than just mounting a git repo - we do make some small changes on top of it during our image build process - which layers do feel quite well suited for. Just still figuring out the best practices for building them out efficiently. :)
>  If a hash was used it would be guaranteed to be consistent across multiple tasks in a service.

A small thing but something that bit me: Consistency isn't currently *always* guaranteed. Example: If you create a container image with some data exposed _in a volume_, when you run the container, the mtime of _folders_ within the volume is set to when the container **was run** rather than when the container image **was created**. Logged in #17018.
@tomfotherby I am not sure how that is at all related here. This proposal has nothing to do with volumes.
@stevvooe - Sorry, Let me explain in more depth.
Lets say this issue is implemented and the immutable data volume that we want to mount into a "web-server container" is website code. The data-container that we are going to mount is built like this:

    FROM scratch
    COPY . /var/www
    VOLUME ["/var/www"]
    ENTRYPOINT ["true"]

Lets say we launch 5 web-servers with the data volume mounted in via `--volumes-from` and they start slowly maybe 1s apart. The problem is the the mtime of the folders in `/var/www` volume will not be consistent across all web-servers. This matters in some situations for example, some web-frameworks build javascript bundles with a filename that is a hash of the contents of the folder, including mtime, meaning each web-server will generate a differently-named bundle and this can break the website.

So until #17018 is resolved, this ticket wouldn't be 100% useful in my use-case. I understand that it's maybe a edge-case and safe to ignore.
I believe (and please correct me if I'm wrong) what you're describing is something like the behaviour of `--volumes-from`. I think this proposal would ignore any volumes in the image. So the data image would be something like this:

```
FROM scratch
COPY . /
```

I suspect the implementation wouldn't have the same mtime issue, because it wouldn't be using volumes.

@dnephin - Thanks for your explanation. Yes I was talking about `--volumes-from`. It's great this implementation wouldn't be using volumes.
I see this proposal as quite risky. I wouldn't want anyone to use it to include any code or other dependencies required for running the image.

The real issue behind this seems to be the composability problems in the builder. The use case to share data is clearly very useful but is should be defined at build time. Having images in the hub that don't work unless the user specifies a specific mountpoint is a very bad UX. An image should define all of its dependencies behind its digest.

If using the current builder is inefficient for this problem and doesn't allow maximum data reuse, it should be fixed instead. One (extreme) way would be to commit the digest references of the mounts into the image config. Then at least in the low level, this would be equal with the current proposal.

I'd like to address the configuration loading separately. It is true that this could be used to load configuration as well, although I don't see it as the main use case. First, it wouldn't be simple - Docker image format is not designed for this, it would be quite hard for the user to generate these "configuration-images" or make modifications to them. In the same time, the "configuration-image" would pollute registry and `docker images` with images that can't be run. I do see the benefits of immutable volumes for loading configuration. I have nothing against loading files directly from github or having a way to create snapshot volumes based on local data that would be available to services. But at the same time, I don't want to expose many new problems.

@tonistiigi I don't see how this can cause any real problems.

> The use case to share data is clearly very useful but is should be defined at build time. 

This is the whole point: not everything is known at build time. This is not a build problem. Deployment is an emergent configuration of build time artifacts and environment. Mounting configuration from a data image is part of that environment. Ultimately, this is a runtime dependency, likely shipped from different teams. Making both teams to coordinate putting different configuration layers on top of each other is the problem (we also see this when understanding the use cases of pods). Time and time again have I seen users struggling with not wanting to push their own configuration on top of a registry image due to operational constraints, only to have them struggle with the distribution of content to make a bind mount.

> First, it wouldn't be simple - Docker image format is not designed for this, it would be quite hard for the user to generate these "configuration-images" or make modifications to them.

Making this statement would be the same as saying docker image format isn't good for _anything_. Generating these images would be as simple as having a copy statement into a docker image. Is there something I am missing here?

> In the same time, the "configuration-image" would pollute registry and docker images with images that can't be run.

This is a minor issue. We could introduce a special type of image for this, if this is a real problem, or we could have them cat the configuration by default or even apply a templating step.

This is a longstanding ask in docker and really does cover a number of use cases. I think the fact that we have had volume plugins for some time but no solutions shows the approach is fundamentally flawed. Personally, I have had nothing but problems with docker volumes and in practice they have solved nothing (people still say "don't run containers with data"). If this were possible with volumes, we would already see plugins that exist, but they aren't there (I have a git implementation, but where are the others?). We can do better.

These immutable _mounts_ can share a massive piece of infrastructure with very little extra work. In fact, we already have the build, the distribution infrastructure, the file format and the command structure in mounts. Let's leverage this and stamp out this use case.
Let's create this convention:

```
FROM config
COPY . /
```
@stevvooe So the mount wouldn't work if it isn't based on that image?
> `FROM config`

I guess it would be useful for non-config data (e.g. large scientific data split from the program container).
So I wonder calling it `config` can be confusing.

How about `FROM scratch` (from anything should be ok) and some label like `LABEL com.docker.image.unrunnable`?


> @stevvooe So the mount wouldn't work if it isn't based on that image?

I don't think we need that limitation. I was just thinking of a convention that we could use to differentiate.

I'm just not understanding why plucking something out of the rootfs out of an image is such a problem.
FYI: now we have `docker run --mount type=...`, and it would be easy to add new mount type


https://github.com/docker/docker/pull/32251

I think it will make sense copying file to a volume. Use case can be a multi stage build that check out a code  build it into an image and just like the COPY --from=build src dest functionality i should be able to do some like:

`docker run -it -v <image-name>:/usr/src  <other-image-name> command ` or 
`docker run --mount type=image src=imageName dest=/usr/src <other-image-name command`

Or in a docker-compose file
I am all for this proposal. What are the next steps?
To me this is important to avoid copying this data from an image, but just directly mount it into a container. So once image is made and has files I would want to expose to another container, I would prefer to have only one copy of data around.
This would probably be interesting in combination with https://github.com/moby/moby/pull/39041 (https://github.com/moby/moby/issues/32582) as well; for example; a service's image that has static files; allowing another service to mount the subdirectory containing the static files from that image
FWIW; `docker build` allows this when using BuildKit (`DOCKER_BUILDKIT=1`) using the experimental [`RUN --mount` syntax](https://github.com/moby/buildkit/blob/aed5e98d92d77c4ff27d882deb25eff9a3e91c4f/frontend/dockerfile/docs/experimental.md#run---mounttypebind-the-default-mount-type) (allowing you to [mount an image](https://gist.github.com/thaJeztah/cfd929a31976b745e3f7515ae37eb192))
@thaJeztah True, but that could also be achieved via multistage build. 
The neat thing having a volume that was populated via image is already stated: reproducibility :)
 
@christiankniep sorry, perhaps my comment was unclear; I meant to say "mounting from an image is not unprecedented; we now allow it in `docker build`, so might as well support it on `docker run` as well"
I came across the same issue when I tried to deploy a PHP application with nginx, as nginx should serve assets directly, while all other requests get passed to the PHP container. I thought about the following options:

1. Build two images for `nginx` and `php-fpm` including the necessary sources. While the PHP image would include not only the sources, but also required dependencies, the nginx image wouldn't have any extras except the same sources. In this scenario I need to maintain two images and can't benefit from fast deployed security fixes upstream in the official images.

2. One could checkout the compiled sources and assets with any other tool and mount them into both containers. Let's assume you like to deploy a simple LDAP phonebook: First anyone who has this application deployed needs to manually update and track updates which doesn't scale. Second, this scenario needs a specialized PHP container with its dependencies installed, too. In addition, the sources and Docker image can't be deployed together anymore, so there might be inconsistencies e.g. when PHP 7.3 code is run in a 7.0 image.

To solve this issue I created [a volume plugin](https://github.com/gridnodes/docker-volume-image) to mount an image layer as volume into other containers. This allows me to maintain just a single application image and mount the assets into a plain `nginx` container. It works, but has some [limitations](https://github.com/gridnodes/docker-volume-image#limitations), most notably that the image is not associated with the running container and thus can be deleted any time.

Another use case for this plugin is to mount configuration images created from `scratch`. For example the `nginx` container could use custom error pages, which can easily be deployed and updated independent of `nginx`, i.e. without interfering their update schedule including security fixes.
Is the problem still there?
Yes, this has not yet been implemented (but contributions welcome!)
I would love this for adding debugging tools to an existing container at runtime, such as adding busybox to a single-binary container. I normally do this with named volumes, but this would provide a sort of "globally named volume".
Chiming in on this with other use cases… I’ve had several customer calls lately where folks are interested in distributing large files (various datasets, squashfs images, etc.). Using OCI artifacts/images makes distribution and caching easy, but getting the data into containers is challenging. 

Sure, you could either do a multi-stage build to combine the image with the large files or populate a volume. But, you’re also then using that much more disk space with the copy. Being able to mount it directly as a read-only mount would be incredibly useful. I imagine as OCI artifacts start to be used more often to distribute config, etc., this use case will continue to rise.

Fix patch:
diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go
index 011808b72106b..5b07db5ebc51c 100644
--- a/api/server/router/container/container_routes.go
+++ b/api/server/router/container/container_routes.go
@@ -649,6 +649,11 @@ func (c *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
 			// reinitialize this field.
 			epConfig.GwPriority = 0
 		}
+		for _, m := range hostConfig.Mounts {
+			if m.Type == mount.TypeImage {
+				return errdefs.InvalidParameter(errors.New(`Mount type "Image" needs API v1.48 or newer`))
+			}
+		}
 	}
 
 	var warnings []string
diff --git a/api/swagger.yaml b/api/swagger.yaml
index 829e9a058e075..5c43b7237bf27 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -212,6 +212,7 @@ definitions:
 
           - `bind` a mount of a file or directory from the host into the container.
           - `volume` a docker volume with the given `Name`.
+          - `image` a docker image
           - `tmpfs` a `tmpfs`.
           - `npipe` a named pipe from the host into the container.
           - `cluster` a Swarm cluster volume
@@ -219,6 +220,7 @@ definitions:
         enum:
           - "bind"
           - "volume"
+          - "image"
           - "tmpfs"
           - "npipe"
           - "cluster"
@@ -350,6 +352,7 @@ definitions:
 
           - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container.
           - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed.
+          - `image` Mounts an image.
           - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs.
           - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container.
           - `cluster` a Swarm cluster volume
@@ -357,6 +360,7 @@ definitions:
         enum:
           - "bind"
           - "volume"
+          - "image"
           - "tmpfs"
           - "npipe"
           - "cluster"
@@ -431,6 +435,14 @@ definitions:
             description: "Source path inside the volume. Must be relative without any back traversals."
             type: "string"
             example: "dir-inside-volume/subdirectory"
+      ImageOptions:
+        description: "Optional configuration for the `image` type."
+        type: "object"
+        properties:
+          Subpath:
+            description: "Source path inside the image. Must be relative without any back traversals."
+            type: "string"
+            example: "dir-inside-image/subdirectory"
       TmpfsOptions:
         description: "Optional configuration for the `tmpfs` type."
         type: "object"
diff --git a/api/types/mount/mount.go b/api/types/mount/mount.go
index c68dcf65bd12d..d98dbec991cf5 100644
--- a/api/types/mount/mount.go
+++ b/api/types/mount/mount.go
@@ -19,6 +19,8 @@ const (
 	TypeNamedPipe Type = "npipe"
 	// TypeCluster is the type for Swarm Cluster Volumes.
 	TypeCluster Type = "cluster"
+	// TypeImage is the type for mounting another image's filesystem
+	TypeImage Type = "image"
 )
 
 // Mount represents a mount (volume).
@@ -34,6 +36,7 @@ type Mount struct {
 
 	BindOptions    *BindOptions    `json:",omitempty"`
 	VolumeOptions  *VolumeOptions  `json:",omitempty"`
+	ImageOptions   *ImageOptions   `json:",omitempty"`
 	TmpfsOptions   *TmpfsOptions   `json:",omitempty"`
 	ClusterOptions *ClusterOptions `json:",omitempty"`
 }
@@ -100,6 +103,10 @@ type VolumeOptions struct {
 	DriverConfig *Driver           `json:",omitempty"`
 }
 
+type ImageOptions struct {
+	Subpath string `json:",omitempty"`
+}
+
 // Driver represents a volume driver.
 type Driver struct {
 	Name    string            `json:",omitempty"`
diff --git a/daemon/containerd/image_delete.go b/daemon/containerd/image_delete.go
index 28a66239c3d35..e952c50586fa0 100644
--- a/daemon/containerd/image_delete.go
+++ b/daemon/containerd/image_delete.go
@@ -151,7 +151,19 @@ func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force,
 		}
 
 		using := func(c *container.Container) bool {
-			return c.ImageID == imgID
+			if c.ImageID == imgID {
+				return true
+			}
+
+			for _, mp := range c.MountPoints {
+				if mp.Type == "image" {
+					if mp.Spec.Source == string(imgID) {
+						return true
+					}
+				}
+			}
+
+			return false
 		}
 		// TODO: Should this also check parentage here?
 		ctr := i.containers.First(using)
diff --git a/daemon/containerd/image_snapshot.go b/daemon/containerd/image_snapshot.go
index 78362e6d3cc3e..24f2fd62b4ecf 100644
--- a/daemon/containerd/image_snapshot.go
+++ b/daemon/containerd/image_snapshot.go
@@ -13,67 +13,72 @@ import (
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/daemon/snapshotter"
 	"github.com/docker/docker/errdefs"
+	"github.com/docker/docker/image"
 	"github.com/docker/docker/layer"
 	"github.com/opencontainers/image-spec/identity"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 )
 
 // CreateLayer creates a new layer for a container.
-// TODO(vvoland): Decouple from container
 func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.MountInit) (container.RWLayer, error) {
-	ctx := context.TODO()
-
-	var parentSnapshot string
+	var descriptor *ocispec.Descriptor
 	if ctr.ImageManifest != nil {
-		img := c8dimages.Image{
-			Target: *ctr.ImageManifest,
-		}
-		platformImg, err := i.NewImageManifest(ctx, img, img.Target)
-		if err != nil {
-			return nil, err
-		}
-		unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
-		if err != nil {
-			return nil, err
-		}
+		descriptor = ctr.ImageManifest
+	}
 
-		if !unpacked {
-			if err := platformImg.Unpack(ctx, i.snapshotter); err != nil {
-				return nil, err
-			}
-		}
+	rwLayerOpts := &layer.CreateRWLayerOpts{
+		StorageOpt: ctr.HostConfig.StorageOpt,
+	}
+
+	return i.createLayer(descriptor, ctr.ID, rwLayerOpts, initFunc)
+}
+
+// CreateLayerFromImage creates a new layer from an image
+func (i *ImageService) CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (container.RWLayer, error) {
+	var descriptor *ocispec.Descriptor
+	if img != nil {
+		descriptor = img.Details.ManifestDescriptor
+	}
 
-		diffIDs, err := platformImg.RootFS(ctx)
+	return i.createLayer(descriptor, layerName, rwLayerOpts, nil)
+}
+
+func (i *ImageService) createLayer(descriptor *ocispec.Descriptor, layerName string, rwLayerOpts *layer.CreateRWLayerOpts, initFunc layer.MountInit) (container.RWLayer, error) {
+	ctx := context.TODO()
+	var parentSnapshot string
+	if descriptor != nil {
+		snapshot, err := i.getImageSnapshot(ctx, descriptor)
 		if err != nil {
 			return nil, err
 		}
-
-		parentSnapshot = identity.ChainID(diffIDs).String()
+		parentSnapshot = snapshot
 	}
 
-	id := ctr.ID
-
 	// TODO: Consider a better way to do this. It is better to have a container directly
 	// reference a snapshot, however, that is not done today because a container may
 	// removed and recreated with nothing holding the snapshot in between. Consider
 	// removing this lease and only temporarily holding a lease on re-create, using
 	// non-expiring leases introduces the possibility of leaking resources.
 	ls := i.client.LeasesService()
-	lease, err := ls.Create(ctx, leases.WithID(id))
+	lease, err := ls.Create(ctx, leases.WithID(layerName))
 	if err != nil {
 		return nil, err
 	}
 	ctx = leases.WithLease(ctx, lease.ID)
 
-	if err := i.prepareInitLayer(ctx, id, parentSnapshot, initFunc); err != nil {
-		return nil, err
+	if initFunc != nil {
+		if err := i.prepareInitLayer(ctx, layerName, parentSnapshot, initFunc); err != nil {
+			return nil, err
+		}
+		parentSnapshot = layerName + "-init"
 	}
 
 	sn := i.client.SnapshotService(i.StorageDriver())
 	if !i.idMapping.Empty() {
-		err = i.remapSnapshot(ctx, sn, id, id+"-init")
+		err = i.remapSnapshot(ctx, sn, layerName, parentSnapshot)
 	} else {
-		_, err = sn.Prepare(ctx, id, id+"-init")
+		_, err = sn.Prepare(ctx, layerName, parentSnapshot)
 	}
 
 	if err != nil {
@@ -81,7 +86,7 @@ func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.Moun
 	}
 
 	return &rwLayer{
-		id:              id,
+		id:              layerName,
 		snapshotterName: i.StorageDriver(),
 		snapshotter:     sn,
 		refCountMounter: i.refCountMounter,
@@ -89,6 +94,37 @@ func (i *ImageService) CreateLayer(ctr *container.Container, initFunc layer.Moun
 	}, nil
 }
 
+func (i *ImageService) getImageSnapshot(ctx context.Context, descriptor *ocispec.Descriptor) (string, error) {
+	c8dImg := c8dimages.Image{
+		Target: *descriptor,
+	}
+
+	platformImg, err := i.NewImageManifest(ctx, c8dImg, c8dImg.Target)
+	if err != nil {
+		return "", err
+	}
+
+	unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
+	if err != nil {
+		return "", err
+	}
+
+	if !unpacked {
+		if err := platformImg.Unpack(ctx, i.snapshotter); err != nil {
+			return "", err
+		}
+	}
+
+	diffIDs, err := platformImg.RootFS(ctx)
+	if err != nil {
+		return "", err
+	}
+
+	snapshot := identity.ChainID(diffIDs).String()
+
+	return snapshot, nil
+}
+
 type rwLayer struct {
 	id              string
 	snapshotter     snapshots.Snapshotter
@@ -188,7 +224,9 @@ func (l *rwLayer) Unmount() error {
 }
 
 func (l rwLayer) Metadata() (map[string]string, error) {
-	return nil, nil
+	return map[string]string{
+		"ID": l.id,
+	}, nil
 }
 
 // ReleaseLayer releases a layer allowing it to be removed
diff --git a/daemon/image_service.go b/daemon/image_service.go
index dfc4a0b8f76e0..a24ffe91a8f48 100644
--- a/daemon/image_service.go
+++ b/daemon/image_service.go
@@ -48,6 +48,7 @@ type ImageService interface {
 
 	GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error)
 	CreateLayer(container *container.Container, initFunc layer.MountInit) (container.RWLayer, error)
+	CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (container.RWLayer, error)
 	GetLayerByID(cid string) (container.RWLayer, error)
 	LayerStoreStatus() [][2]string
 	GetLayerMountID(cid string) (string, error)
diff --git a/daemon/images/image_delete.go b/daemon/images/image_delete.go
index 00d84ee4f3fdb..552ec722d96bc 100644
--- a/daemon/images/image_delete.go
+++ b/daemon/images/image_delete.go
@@ -75,7 +75,19 @@ func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force,
 	repoRefs := i.referenceStore.References(imgID.Digest())
 
 	using := func(c *container.Container) bool {
-		return c.ImageID == imgID
+		if c.ImageID == imgID {
+			return true
+		}
+
+		for _, mp := range c.MountPoints {
+			if mp.Type == "image" {
+				if mp.Spec.Source == string(imgID) {
+					return true
+				}
+			}
+		}
+
+		return false
 	}
 
 	var removedRepositoryRef bool
diff --git a/daemon/images/service.go b/daemon/images/service.go
index 213eec28bd23b..1c6167197c981 100644
--- a/daemon/images/service.go
+++ b/daemon/images/service.go
@@ -119,13 +119,13 @@ func (i *ImageService) Children(_ context.Context, id image.ID) ([]image.ID, err
 // called from create.go
 // TODO: accept an opt struct instead of container?
 func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (container.RWLayer, error) {
-	var layerID layer.ChainID
+	var img *image.Image
 	if container.ImageID != "" {
-		img, err := i.imageStore.Get(container.ImageID)
+		containerImg, err := i.imageStore.Get(container.ImageID)
 		if err != nil {
 			return nil, err
 		}
-		layerID = img.RootFS.ChainID()
+		img = containerImg
 	}
 
 	rwLayerOpts := &layer.CreateRWLayerOpts{
@@ -134,7 +134,18 @@ func (i *ImageService) CreateLayer(container *container.Container, initFunc laye
 		StorageOpt: container.HostConfig.StorageOpt,
 	}
 
-	return i.layerStore.CreateRWLayer(container.ID, layerID, rwLayerOpts)
+	return i.CreateLayerFromImage(img, container.ID, rwLayerOpts)
+}
+
+// CreateLayerFromImage creates a file system from an arbitrary image
+// Used to mount an image inside another
+func (i *ImageService) CreateLayerFromImage(img *image.Image, layerName string, rwLayerOpts *layer.CreateRWLayerOpts) (container.RWLayer, error) {
+	var layerID layer.ChainID
+	if img != nil {
+		layerID = img.RootFS.ChainID()
+	}
+
+	return i.layerStore.CreateRWLayer(layerName, layerID, rwLayerOpts)
 }
 
 // GetLayerByID returns a layer by ID
diff --git a/daemon/mounts.go b/daemon/mounts.go
index cfc27052ace40..9196cee0dfcab 100644
--- a/daemon/mounts.go
+++ b/daemon/mounts.go
@@ -17,6 +17,17 @@ func (daemon *Daemon) prepareMountPoints(container *container.Container) error {
 		if err := daemon.lazyInitializeVolume(container.ID, config); err != nil {
 			return err
 		}
+
+		// Restore reference to image mount layer
+		if config.Type == mounttypes.TypeImage && config.Layer == nil {
+			layer, err := daemon.imageService.GetLayerByID(config.ID)
+			if err != nil {
+				return err
+			}
+
+			config.Layer = layer
+		}
+
 		if config.Volume == nil {
 			// FIXME(thaJeztah): should we check for config.Type here as well? (i.e., skip bind-mounts etc)
 			continue
@@ -38,28 +49,48 @@ func (daemon *Daemon) removeMountPoints(container *container.Container, rm bool)
 	var rmErrors []string
 	ctx := context.TODO()
 	for _, m := range container.MountPoints {
-		if m.Type != mounttypes.TypeVolume || m.Volume == nil {
-			continue
-		}
-		daemon.volumes.Release(ctx, m.Volume.Name(), container.ID)
-		if !rm {
-			continue
-		}
+		if m.Type == mounttypes.TypeVolume {
+			if m.Volume == nil {
+				continue
+			}
+			daemon.volumes.Release(ctx, m.Volume.Name(), container.ID)
+			if !rm {
+				continue
+			}
 
-		// Do not remove named mountpoints
-		// these are mountpoints specified like `docker run -v <name>:/foo`
-		if m.Spec.Source != "" {
-			continue
+			// Do not remove named mountpoints
+			// these are mountpoints specified like `docker run -v <name>:/foo`
+			if m.Spec.Source != "" {
+				continue
+			}
+
+			err := daemon.volumes.Remove(ctx, m.Volume.Name())
+			// Ignore volume in use errors because having this
+			// volume being referenced by other container is
+			// not an error, but an implementation detail.
+			// This prevents docker from logging "ERROR: Volume in use"
+			// where there is another container using the volume.
+			if err != nil && !volumesservice.IsInUse(err) {
+				rmErrors = append(rmErrors, err.Error())
+			}
 		}
 
-		err := daemon.volumes.Remove(ctx, m.Volume.Name())
-		// Ignore volume in use errors because having this
-		// volume being referenced by other container is
-		// not an error, but an implementation detail.
-		// This prevents docker from logging "ERROR: Volume in use"
-		// where there is another container using the volume.
-		if err != nil && !volumesservice.IsInUse(err) {
-			rmErrors = append(rmErrors, err.Error())
+		if m.Type == mounttypes.TypeImage {
+			layer := m.Layer
+			if layer != nil {
+				err := layer.Unmount()
+				if err != nil {
+					rmErrors = append(rmErrors, err.Error())
+					continue
+				}
+				err = daemon.imageService.ReleaseLayer(layer)
+				if err != nil {
+					rmErrors = append(rmErrors, err.Error())
+					continue
+				}
+			} else {
+				rmErrors = append(rmErrors, fmt.Sprintf("layer not found for image %s", m.Name))
+			}
 		}
 	}
 
diff --git a/daemon/volumes.go b/daemon/volumes.go
index e48dacf961723..f42465d60c45b 100644
--- a/daemon/volumes.go
+++ b/daemon/volumes.go
@@ -2,6 +2,7 @@ package daemon // import "github.com/docker/docker/daemon"
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"path/filepath"
 	"sort"
@@ -9,12 +10,14 @@ import (
 	"time"
 
 	"github.com/containerd/log"
+	"github.com/docker/docker/api/types/backend"
 	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/mount"
 	mounttypes "github.com/docker/docker/api/types/mount"
 	volumetypes "github.com/docker/docker/api/types/volume"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/errdefs"
+	"github.com/docker/docker/layer"
 	"github.com/docker/docker/volume"
 	volumemounts "github.com/docker/docker/volume/mounts"
 	"github.com/docker/docker/volume/service"
@@ -245,6 +248,42 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
 			}
 		}
 
+		if mp.Type == mounttypes.TypeImage {
+			img, err := daemon.imageService.GetImage(ctx, mp.Source, backend.GetImageOpts{})
+			if err != nil {
+				return err
+			}
+
+			rwLayerOpts := &layer.CreateRWLayerOpts{
+				StorageOpt: container.HostConfig.StorageOpt,
+			}
+
+			layerName := fmt.Sprintf("%s-%s", container.ID, mp.Source)
+			layer, err := daemon.imageService.CreateLayerFromImage(img, layerName, rwLayerOpts)
+			if err != nil {
+				return err
+			}
+			metadata, err := layer.Metadata()
+			if err != nil {
+				return err
+			}
+
+			path, err := layer.Mount("")
+			if err != nil {
+				return err
+			}
+
+			if metadata["ID"] != "" {
+				mp.ID = metadata["ID"]
+			}
+
+			mp.Name = mp.Spec.Source
+			mp.Spec.Source = img.ID().String()
+			mp.Source = path
+			mp.Layer = layer
+			mp.RW = false
+		}
+
 		binds[mp.Destination] = true
 		dereferenceIfExists(mp.Destination)
 		mountPoints[mp.Destination] = mp
diff --git a/docs/api/version-history.md b/docs/api/version-history.md
index b0b02482a656d..bab01957bccc4 100644
--- a/docs/api/version-history.md
+++ b/docs/api/version-history.md
@@ -38,6 +38,8 @@ keywords: "API, Docker, rcli, REST, documentation"
   defined through `Mounts` because the `VolumeDriver` option has no effect on
   those volumes. This warning was previously generated by the CLI, but now
   moved to the daemon so that other clients can also get this warning.
+* `POST /containers/create` now supports `Mount` of type `image` for mounting
+  an image inside a container.
 * Deprecated: The `ContainerdCommit.Expected`, `RuncCommit.Expected`, and
   `InitCommit.Expected` fields in the `GET /info` endpoint are deprecated
   and will be omitted in API v1.49.
diff --git a/layer/mounted_layer.go b/layer/mounted_layer.go
index 05f98f5f3d1d2..0e6d3655b3da2 100644
--- a/layer/mounted_layer.go
+++ b/layer/mounted_layer.go
@@ -55,7 +55,20 @@ func (ml *mountedLayer) Changes() ([]archive.Change, error) {
 }
 
 func (ml *mountedLayer) Metadata() (map[string]string, error) {
-	return ml.layerStore.driver.GetMetadata(ml.mountID)
+	m, err := ml.layerStore.driver.GetMetadata(ml.mountID)
+	if err != nil {
+		return nil, err
+	}
+
+	if m == nil {
+		m = make(map[string]string)
+	}
+
+	if m["ID"] == "" {
+		m["ID"] = ml.name
+	}
+
+	return m, nil
 }
 
 func (ml *mountedLayer) getReference() RWLayer {
diff --git a/volume/mounts/linux_parser.go b/volume/mounts/linux_parser.go
index 507003a08692a..72b26132fcfcd 100644
--- a/volume/mounts/linux_parser.go
+++ b/volume/mounts/linux_parser.go
@@ -74,6 +74,9 @@ func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSour
 		if mnt.VolumeOptions != nil {
 			return &errMountConfig{mnt, errExtraField("VolumeOptions")}
 		}
+		if mnt.ImageOptions != nil {
+			return &errMountConfig{mnt, errExtraField("ImageOptions")}
+		}
 
 		if err := linuxValidateAbsolute(mnt.Source); err != nil {
 			return &errMountConfig{mnt, err}
@@ -95,6 +98,9 @@ func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSour
 		if mnt.BindOptions != nil {
 			return &errMountConfig{mnt, errExtraField("BindOptions")}
 		}
+		if mnt.ImageOptions != nil {
+			return &errMountConfig{mnt, errExtraField("ImageOptions")}
+		}
 		anonymousVolume := len(mnt.Source) == 0
 
 		if mnt.VolumeOptions != nil && mnt.VolumeOptions.Subpath != "" {
@@ -113,12 +119,30 @@ func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSour
 		if mnt.BindOptions != nil {
 			return &errMountConfig{mnt, errExtraField("BindOptions")}
 		}
+		if mnt.ImageOptions != nil {
+			return &errMountConfig{mnt, errExtraField("ImageOptions")}
+		}
 		if len(mnt.Source) != 0 {
 			return &errMountConfig{mnt, errExtraField("Source")}
 		}
 		if _, err := p.ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
 			return &errMountConfig{mnt, err}
 		}
+	case mount.TypeImage:
+		if mnt.BindOptions != nil {
+			return &errMountConfig{mnt, errExtraField("BindOptions")}
+		}
+		if mnt.VolumeOptions != nil {
+			return &errMountConfig{mnt, errExtraField("VolumeOptions")}
+		}
+		if len(mnt.Source) == 0 {
+			return &errMountConfig{mnt, errMissingField("Source")}
+		}
+		if mnt.ImageOptions != nil && mnt.ImageOptions.Subpath != "" {
+			if !filepath.IsLocal(mnt.ImageOptions.Subpath) {
+				return &errMountConfig{mnt, errInvalidSubpath}
+			}
+		}
 	default:
 		return &errMountConfig{mnt, errors.New("mount type unknown")}
 	}
@@ -353,6 +377,15 @@ func (p *linuxParser) parseMountSpec(cfg mount.Mount, validateBindSourceExists b
 		}
 	case mount.TypeTmpfs:
 		// NOP
+	case mount.TypeImage:
+		mp.Source = cfg.Source
+		if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
+			mp.Propagation = cfg.BindOptions.Propagation
+		} else {
+			// If user did not specify a propagation mode, get
+			// default propagation mode.
+			mp.Propagation = linuxDefaultPropagationMode
+		}
 	}
 	return mp, nil
 }
diff --git a/volume/mounts/mounts.go b/volume/mounts/mounts.go
index 7a518a046f2c1..276d972d4cb95 100644
--- a/volume/mounts/mounts.go
+++ b/volume/mounts/mounts.go
@@ -18,6 +18,21 @@ import (
 	"github.com/pkg/errors"
 )
 
+// RWLayer represents a writable layer.
+type RWLayer interface {
+	// Mount mounts the RWLayer and returns the filesystem path
+	// to the writable layer.
+	Mount(mountLabel string) (string, error)
+
+	// Unmount unmounts the RWLayer. This should be called
+	// for every mount. If there are multiple mount calls
+	// this operation will only decrement the internal mount counter.
+	Unmount() error
+
+	// Metadata returns the low level metadata for the mutable layer
+	Metadata() (map[string]string, error)
+}
+
 // MountPoint is the intersection point between a volume and a container. It
 // specifies which volume is to be used and where inside a container it should
 // be mounted.
@@ -80,6 +95,8 @@ type MountPoint struct {
 	// SafePaths created by Setup that should be cleaned up before unmounting
 	// the volume.
 	safePaths []*safepath.SafePath
+
+	Layer RWLayer `json:"-"`
 }
 
 // Cleanup frees resources used by the mountpoint and cleans up all the paths
@@ -202,6 +219,20 @@ func (m *MountPoint) Setup(ctx context.Context, mountLabel string, rootIDs idtoo
 		return volumePath, clean, nil
 	}
 
+	if m.Type == mounttypes.TypeImage {
+		if m.Spec.ImageOptions != nil && m.Spec.ImageOptions.Subpath != "" {
+			subpath := m.Spec.ImageOptions.Subpath
+
+			safePath, err := safepath.Join(ctx, m.Source, subpath)
+			if err != nil {
+				return "", noCleanup, err
+			}
+			m.safePaths = append(m.safePaths, safePath)
+			log.G(ctx).Debugf("mounting (%s|%s) via %s", m.Source, subpath, safePath.Path())
+			return safePath.Path(), safePath.Close, nil
+		}
+	}
+
 	if len(m.Source) == 0 {
 		return "", noCleanup, fmt.Errorf("Unable to setup mount point, neither source nor volume defined")
 	}


Test patch:
diff --git a/integration/daemon/daemon_test.go b/integration/daemon/daemon_test.go
index 8f15776fa4a31..f8c2b2ca16524 100644
--- a/integration/daemon/daemon_test.go
+++ b/integration/daemon/daemon_test.go
@@ -652,6 +652,54 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
 		assert.NilError(t, err)
 	})
 
+	t.Run("image mount", func(t *testing.T) {
+		ctx := testutil.StartSpan(ctx, t)
+
+		mountedImage := "hello-world:frozen"
+		d.LoadImage(ctx, t, mountedImage)
+
+		m := mount.Mount{
+			Type:   mount.TypeImage,
+			Source: mountedImage,
+			Target: "/image",
+		}
+
+		cID := container.Run(ctx, t, c, container.WithMount(m))
+		defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
+
+		waitFn := func(t poll.LogT) poll.Result {
+			_, err := c.ContainerStatPath(ctx, cID, "/image/hello")
+			if err != nil {
+				if errdefs.IsNotFound(err) {
+					return poll.Continue("file doesn't yet exist")
+				}
+				return poll.Error(err)
+			}
+
+			return poll.Success()
+		}
+
+		poll.WaitOn(t, waitFn)
+
+		d.Restart(t, "--live-restore", "--iptables=false", "--ip6tables=false")
+
+		t.Run("image still mounted", func(t *testing.T) {
+			skip.If(t, testEnv.IsRootless(), "restarted rootless daemon has a new mount namespace and it won't have the previous mounts")
+			poll.WaitOn(t, waitFn)
+		})
+
+		_, err := c.ImageRemove(ctx, mountedImage, image.RemoveOptions{})
+		assert.ErrorContains(t, err, fmt.Sprintf("container %s is using its referenced image", cID[:12]))
+
+		// Remove that container which should free the references in the volume
+		err = c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
+		assert.NilError(t, err)
+
+		// Now we should be able to remove the volume
+		_, err = c.ImageRemove(ctx, mountedImage, image.RemoveOptions{})
+		assert.NilError(t, err)
+	})
+
 	// Make sure that we don't panic if the container has bind-mounts
 	// (which should not be "restored")
 	// Regression test for https://github.com/moby/moby/issues/45898
diff --git a/integration/volume/mount_test.go b/integration/volume/mount_test.go
index 1c6b4cd507557..0ffcfe47462d5 100644
--- a/integration/volume/mount_test.go
+++ b/integration/volume/mount_test.go
@@ -1,12 +1,17 @@
 package volume
 
 import (
+	"bytes"
 	"context"
+	"fmt"
+	"io"
 	"path/filepath"
 	"strings"
 	"testing"
 
+	"github.com/docker/docker/api/types"
 	containertypes "github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/image"
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/versions"
@@ -14,6 +19,7 @@ import (
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration/internal/container"
 	"github.com/docker/docker/internal/safepath"
+	"github.com/docker/docker/testutil/fakecontext"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
 	"gotest.tools/v3/skip"
@@ -112,6 +118,114 @@ func TestRunMountVolumeSubdir(t *testing.T) {
 	}
 }
 
+func TestRunMountImage(t *testing.T) {
+	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "skip test from new feature")
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "image mounts not supported on Windows")
+
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
+
+	for _, tc := range []struct {
+		name         string
+		opts         mount.ImageOptions
+		cmd          []string
+		createErr    string
+		startErr     string
+		expected     string
+		skipPlatform string
+	}{
+		{name: "image", cmd: []string{"cat", "/image/foo"}, expected: "bar"},
+		{name: "image_tag", cmd: []string{"cat", "/image/foo"}, expected: "bar"},
+
+		{name: "subdir", opts: mount.ImageOptions{Subpath: "subdir"}, cmd: []string{"ls", "/image"}, expected: "hello"},
+		{name: "subdir link", opts: mount.ImageOptions{Subpath: "hack/good"}, cmd: []string{"ls", "/image"}, expected: "hello"},
+		{name: "subdir link outside context", opts: mount.ImageOptions{Subpath: "hack/bad"}, cmd: []string{"ls", "/image"}, startErr: (&safepath.ErrEscapesBase{}).Error()},
+		{name: "file", opts: mount.ImageOptions{Subpath: "subdir/hello"}, cmd: []string{"cat", "/image"}, expected: "world"},
+		{name: "relative with backtracks", opts: mount.ImageOptions{Subpath: "../../../../../../etc/passwd"}, cmd: []string{"cat", "/image"}, createErr: "subpath must be a relative path within the volume"},
+		{name: "not existing", opts: mount.ImageOptions{Subpath: "not-existing-path"}, cmd: []string{"cat", "/image"}, startErr: (&safepath.ErrNotAccessible{}).Error()},
+
+		{name: "image_remove", cmd: []string{"cat", "/image/foo"}, expected: "bar"},
+		// Expected is duplicated because the container runs twice
+		{name: "image_remove_force", cmd: []string{"cat", "/image/foo"}, expected: "barbar"},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			testImage := setupTestImage(t, ctx, apiClient, tc.name, testEnv.UsingSnapshotter())
+			if testImage != "" {
+				defer apiClient.ImageRemove(ctx, testImage, image.RemoveOptions{Force: true})
+			}
+
+			cfg := containertypes.Config{
+				Image: "busybox",
+				Cmd:   tc.cmd,
+			}
+
+			hostCfg := containertypes.HostConfig{
+				Mounts: []mount.Mount{
+					{
+						Type:         mount.TypeImage,
+						Source:       testImage,
+						Target:       "/image",
+						ImageOptions: &tc.opts,
+					},
+				},
+			}
+
+			startContainer := func(id string) {
+				startErr := apiClient.ContainerStart(ctx, id, containertypes.StartOptions{})
+				if tc.startErr != "" {
+					assert.ErrorContains(t, startErr, tc.startErr)
+					return
+				}
+				assert.NilError(t, startErr)
+			}
+
+			ctrName := strings.ReplaceAll(t.Name(), "/", "_")
+			create, creatErr := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, &network.NetworkingConfig{}, nil, ctrName)
+			id := create.ID
+			if id != "" {
+				defer apiClient.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
+			}
+
+			if tc.createErr != "" {
+				assert.ErrorContains(t, creatErr, tc.createErr)
+				return
+			}
+			assert.NilError(t, creatErr, "container creation failed")
+			startContainer(id)
+
+			// Test image mounted is in use logic
+			if tc.name == "image_remove" {
+				img, _, _ := apiClient.ImageInspectWithRaw(ctx, testImage)
+				imgId := strings.Split(img.ID, ":")[1]
+				_, removeErr := apiClient.ImageRemove(ctx, testImage, image.RemoveOptions{})
+				assert.ErrorContains(t, removeErr, fmt.Sprintf(`container %s is using its referenced image %s`, id[:12], imgId[:12]))
+			}
+
+			// Test that the container servives a restart when mounted image is removed
+			if tc.name == "image_remove_force" {
+				stopErr := apiClient.ContainerStop(ctx, id, containertypes.StopOptions{})
+				assert.NilError(t, stopErr)
+
+				_, removeErr := apiClient.ImageRemove(ctx, testImage, image.RemoveOptions{Force: true})
+				assert.NilError(t, removeErr)
+
+				startContainer(id)
+			}
+
+			output, err := container.Output(ctx, apiClient, id)
+			assert.Check(t, err)
+
+			inspect, err := apiClient.ContainerInspect(ctx, id)
+			if tc.startErr == "" && assert.Check(t, err) {
+				assert.Check(t, is.Equal(inspect.State.ExitCode, 0))
+			}
+
+			assert.Check(t, is.Equal(strings.TrimSpace(output.Stderr), ""))
+			assert.Check(t, is.Equal(strings.TrimSpace(output.Stdout), tc.expected))
+		})
+	}
+}
+
 // setupTestVolume sets up a volume with:
 // .
 // |-- bar.txt                        (file with "foo")
@@ -183,3 +297,62 @@ func setupTestVolume(t *testing.T, client client.APIClient) string {
 
 	return volumeName
 }
+
+func setupTestImage(t *testing.T, ctx context.Context, client client.APIClient, test string, snapshotter bool) string {
+	imgName := "test-image"
+
+	if test == "image_tag" {
+		imgName += ":foo"
+	}
+
+	var symlink string
+	if snapshotter {
+		symlink = "../../../../rootfs"
+	} else {
+		symlink = "../../../../../docker"
+	}
+
+	//nolint:dupword // ignore "Duplicate words (subdir) found (dupword)"
+	dockerfile := `
+		FROM busybox as symlink
+		RUN mkdir /hack \
+			&& ln -s "../subdir" /hack/good \
+			&& ln -s "` + symlink + `" /hack/bad
+		#--
+		FROM scratch
+		COPY foo /
+		COPY subdir subdir
+		COPY --from=symlink /hack /hack
+		`
+
+	source := fakecontext.New(
+		t,
+		"",
+		fakecontext.WithDockerfile(dockerfile),
+		fakecontext.WithFile("foo", "bar"),
+		fakecontext.WithFile("subdir/hello", "world"),
+	)
+	defer source.Close()
+
+	resp, err := client.ImageBuild(ctx,
+		source.AsTarReader(t),
+		types.ImageBuildOptions{
+			Remove:      false,
+			ForceRemove: false,
+			Tags:        []string{imgName},
+		})
+	assert.NilError(t, err)
+
+	out := bytes.NewBuffer(nil)
+	_, err = io.Copy(out, resp.Body)
+	assert.Check(t, resp.Body.Close())
+	assert.NilError(t, err)
+
+	_, _, err = client.ImageInspectWithRaw(ctx, imgName)
+	if err != nil {
+		t.Log(out)
+	}
+	assert.NilError(t, err)
+
+	return imgName
+}
diff --git a/testutil/daemon/daemon.go b/testutil/daemon/daemon.go
index ed3267294b142..c786c1d4993b9 100644
--- a/testutil/daemon/daemon.go
+++ b/testutil/daemon/daemon.go
@@ -856,20 +856,24 @@ func (d *Daemon) SetEnvVar(name, val string) {
 
 // LoadBusybox image into the daemon
 func (d *Daemon) LoadBusybox(ctx context.Context, t testing.TB) {
+	d.LoadImage(ctx, t, "busybox:latest")
+}
+
+func (d *Daemon) LoadImage(ctx context.Context, t testing.TB, img string) {
 	t.Helper()
 	clientHost, err := client.NewClientWithOpts(client.FromEnv)
 	assert.NilError(t, err, "[%s] failed to create client", d.id)
 	defer clientHost.Close()
 
-	reader, err := clientHost.ImageSave(ctx, []string{"busybox:latest"}, image.SaveOptions{})
-	assert.NilError(t, err, "[%s] failed to download busybox", d.id)
+	reader, err := clientHost.ImageSave(ctx, []string{img}, image.SaveOptions{})
+	assert.NilError(t, err, "[%s] failed to download %s", d.id, img)
 	defer reader.Close()
 
 	c := d.NewClientT(t)
 	defer c.Close()
 
 	resp, err := c.ImageLoad(ctx, reader, image.LoadOptions{Quiet: true})
-	assert.NilError(t, err, "[%s] failed to load busybox", d.id)
+	assert.NilError(t, err, "[%s] failed to load %s", d.id, img)
 	defer resp.Body.Close()
 }
 
diff --git a/volume/mounts/parser_test.go b/volume/mounts/parser_test.go
index bfafffdf7e824..0adb6927dbb9a 100644
--- a/volume/mounts/parser_test.go
+++ b/volume/mounts/parser_test.go
@@ -2,6 +2,7 @@ package mounts // import "github.com/docker/docker/volume/mounts"
 
 import (
 	"os"
+	"runtime"
 	"testing"
 
 	"github.com/docker/docker/api/types/mount"
@@ -78,6 +79,16 @@ func TestParseMountSpec(t *testing.T) {
 		},
 	}
 
+	if runtime.GOOS != "windows" {
+		tests = append(tests, struct {
+			input    mount.Mount
+			expected MountPoint
+		}{
+			input:    mount.Mount{Type: mount.TypeImage, Source: "alpine", Target: testDestinationPath},
+			expected: MountPoint{Type: mount.TypeImage, Source: "alpine", Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()},
+		})
+	}
+
 	for _, tc := range tests {
 		t.Run("", func(t *testing.T) {
 			mp, err := parser.ParseMountSpec(tc.input)
diff --git a/volume/mounts/validate_test.go b/volume/mounts/validate_test.go
index f0d8e8a5e9812..c40a64c7c1029 100644
--- a/volume/mounts/validate_test.go
+++ b/volume/mounts/validate_test.go
@@ -31,6 +31,10 @@ func TestValidateMount(t *testing.T) {
 		{
 			input: mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath, Source: "hello", VolumeOptions: &mount.VolumeOptions{Subpath: "world"}},
 		},
+		{
+			input:    mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath, Source: "hello", BindOptions: &mount.BindOptions{}},
+			expected: errExtraField("BindOptions"),
+		},
 		{
 			input:    mount.Mount{Type: mount.TypeBind},
 			expected: errMissingField("Target"),
@@ -55,6 +59,47 @@ func TestValidateMount(t *testing.T) {
 			expected: errBindSourceDoesNotExist(testSourcePath),
 		},
 	}
+
+	if runtime.GOOS != "windows" {
+		imageTests := []struct {
+			input    mount.Mount
+			expected error
+		}{
+			{
+				input:    mount.Mount{Type: mount.TypeImage},
+				expected: errMissingField("Target"),
+			},
+			{
+				input:    mount.Mount{Type: mount.TypeImage, Target: testDestinationPath},
+				expected: errMissingField("Source"),
+			},
+			{
+				input: mount.Mount{Type: mount.TypeImage, Target: testDestinationPath, Source: "hello"},
+			},
+			{
+				input: mount.Mount{Type: mount.TypeImage, Target: testDestinationPath, Source: "hello", ImageOptions: &mount.ImageOptions{Subpath: "world"}},
+			},
+			{
+				input:    mount.Mount{Type: mount.TypeImage, Target: testDestinationPath, Source: "hello", BindOptions: &mount.BindOptions{}},
+				expected: errExtraField("BindOptions"),
+			},
+			{
+				input:    mount.Mount{Type: mount.TypeImage, Target: testDestinationPath, Source: "hello", VolumeOptions: &mount.VolumeOptions{}},
+				expected: errExtraField("VolumeOptions"),
+			},
+
+			{
+				input:    mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath, Source: "hello", ImageOptions: &mount.ImageOptions{}},
+				expected: errExtraField("ImageOptions"),
+			},
+			{
+				input:    mount.Mount{Type: mount.TypeBind, Target: testDestinationPath, Source: testSourcePath, ImageOptions: &mount.ImageOptions{}},
+				expected: errExtraField("ImageOptions"),
+			},
+		}
+		tests = append(tests, imageTests...)
+	}
+
 	for _, tc := range tests {
 		t.Run("", func(t *testing.T) {
 			err := parser.ValidateMountConfig(&tc.input)