Repository: docker/compose (go)

Issue:
[BUG] Unable to bind mount when mount source is within the daemon root
### Description

After upgrading Compose v2.29.6 from v2.29.2, the following service began failing:

```
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
```

Running `docker compose up -d` gives the following error message:

```
Error response from daemon: invalid mount config: must use either propagation mode "rslave" or "rshared" when mount source is within the daemon root, daemon root: "/var/lib/docker", bind mount source: "/", propagation: "rprivate"
```

### Steps To Reproduce

_No response_

### Compose Version

```Text
Docker Compose version v2.29.6
```


### Docker Environment

```Text
Client: Docker Engine - Community
 Version:    27.3.0
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.17.1
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.29.6
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 1
 Server Version: 26.1.3
 Storage Driver: vfs
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8b3b7ca2e5ce38e8f31a34f35b2b68ceb8470d89
 runc version: v1.1.14-0-g2c9f560
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.6.16-linuxkit
 Operating System: Ubuntu 20.04.6 LTS
 OSType: linux
 Architecture: aarch64
 CPUs: 12
 Total Memory: 7.657GiB
 Name: d4d8c60ee1fe
 ID: 186ebd80-8c59-49b8-92ae-007c1c620318
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```


### Anything else?

_No response_

Pull request description:
revert commits link to mount API over bind changes
**What I did**
I reverted the following commits:

- c16df17e1f2022c907a68b010f380bce53ac3e36
- cb00aaad288ff83ed98835a1edea635800f5b083
- f9c7a0cc085eba94d6140dcae18f16d7ef5a7691


**Related issue**
<!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
fixes #12139 #12140

**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
![image](https://github.com/user-attachments/assets/9cc03712-3680-4bca-ab86-b8df60222d5a)


Patch:
diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go
index a93c95bc2..10322fc7d 100644
--- a/pkg/compose/compose.go
+++ b/pkg/compose/compose.go
@@ -321,23 +321,6 @@ func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
 
 }
 
-var windowsContainer = struct {
-	once sync.Once
-	val  bool
-	err  error
-}{}
-
-func (s *composeService) isWindowsContainer(ctx context.Context) (bool, error) {
-	windowsContainer.once.Do(func() {
-		info, err := s.apiClient().Info(ctx)
-		if err != nil {
-			windowsContainer.err = err
-		}
-		windowsContainer.val = info.OSType == "windows"
-	})
-	return windowsContainer.val, windowsContainer.err
-}
-
 func (s *composeService) isDesktopIntegrationActive() bool {
 	return s.desktopCli != nil
 }
diff --git a/pkg/compose/create.go b/pkg/compose/create.go
index 08b9a0aa4..c4d1734fa 100644
--- a/pkg/compose/create.go
+++ b/pkg/compose/create.go
@@ -801,28 +801,28 @@ func (s *composeService) buildContainerVolumes(
 		return nil, nil, err
 	}
 
-	mountOptions, err := s.buildContainerMountOptions(ctx, p, service, imgInspect, inherit)
+	mountOptions, err := buildContainerMountOptions(p, service, imgInspect, inherit)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	version, err := s.RuntimeVersion(ctx)
-	if err != nil {
-		return nil, nil, err
-	}
-	if versions.GreaterThan(version, "1.42") {
-		// We can fully leverage `Mount` API as a replacement for legacy `Bind`
-		return nil, mountOptions, nil
-	}
-
 MOUNTS:
 	for _, m := range mountOptions {
+		if m.Type == mount.TypeNamedPipe {
+			mounts = append(mounts, m)
+			continue
+		}
 		if m.Type == mount.TypeBind {
-			// `Mount` does not offer option to created host path if missing
+			// `Mount` is preferred but does not offer option to created host path if missing
 			// so `Bind` API is used here with raw volume string
+			// see https://github.com/moby/moby/issues/43483
 			for _, v := range service.Volumes {
 				if v.Target == m.Target {
-					if v.Bind != nil && v.Bind.CreateHostPath {
+					switch {
+					case string(m.Type) != v.Type:
+						v.Source = m.Source
+						fallthrough
+					case v.Bind != nil && v.Bind.CreateHostPath:
 						binds = append(binds, v.String())
 						continue MOUNTS
 					}
@@ -834,7 +834,7 @@ MOUNTS:
 	return binds, mounts, nil
 }
 
-func (s *composeService) buildContainerMountOptions(ctx context.Context, p types.Project, service types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
+func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
 	var mounts = map[string]mount.Mount{}
 	if inherit != nil {
 		for _, m := range inherit.Mounts {
@@ -859,7 +859,7 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
 				}
 			}
 			volumes := []types.ServiceVolumeConfig{}
-			for _, v := range service.Volumes {
+			for _, v := range s.Volumes {
 				if v.Target != m.Destination || v.Source != "" {
 					volumes = append(volumes, v)
 					continue
@@ -872,11 +872,11 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
 					ReadOnly: !m.RW,
 				}
 			}
-			service.Volumes = volumes
+			s.Volumes = volumes
 		}
 	}
 
-	mounts, err := s.fillBindMounts(ctx, p, service, mounts)
+	mounts, err := fillBindMounts(p, s, mounts)
 	if err != nil {
 		return nil, err
 	}
@@ -888,27 +888,27 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
 	return values, nil
 }
 
-func (s *composeService) fillBindMounts(ctx context.Context, p types.Project, service types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
-	for _, v := range service.Volumes {
-		bindMount, err := s.buildMount(ctx, p, v)
+func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
+	for _, v := range s.Volumes {
+		bindMount, err := buildMount(p, v)
 		if err != nil {
 			return nil, err
 		}
 		m[bindMount.Target] = bindMount
 	}
 
-	secrets, err := s.buildContainerSecretMounts(ctx, p, service)
+	secrets, err := buildContainerSecretMounts(p, s)
 	if err != nil {
 		return nil, err
 	}
-	for _, secret := range secrets {
-		if _, found := m[secret.Target]; found {
+	for _, s := range secrets {
+		if _, found := m[s.Target]; found {
 			continue
 		}
-		m[secret.Target] = secret
+		m[s.Target] = s
 	}
 
-	configs, err := s.buildContainerConfigMounts(ctx, p, service)
+	configs, err := buildContainerConfigMounts(p, s)
 	if err != nil {
 		return nil, err
 	}
@@ -921,11 +921,11 @@ func (s *composeService) fillBindMounts(ctx context.Context, p types.Project, se
 	return m, nil
 }
 
-func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
+func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
 	var mounts = map[string]mount.Mount{}
 
 	configsBaseDir := "/"
-	for _, config := range service.Configs {
+	for _, config := range s.Configs {
 		target := config.Target
 		if config.Target == "" {
 			target = configsBaseDir + config.Source
@@ -953,7 +953,7 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
 			continue
 		}
 
-		bindMount, err := s.buildMount(ctx, p, types.ServiceVolumeConfig{
+		bindMount, err := buildMount(p, types.ServiceVolumeConfig{
 			Type:     types.VolumeTypeBind,
 			Source:   definedConfig.File,
 			Target:   target,
@@ -971,11 +971,11 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
 	return values, nil
 }
 
-func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
+func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
 	var mounts = map[string]mount.Mount{}
 
 	secretsDir := "/run/secrets/"
-	for _, secret := range service.Secrets {
+	for _, secret := range s.Secrets {
 		target := secret.Target
 		if secret.Target == "" {
 			target = secretsDir + secret.Source
@@ -1003,7 +1003,7 @@ func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types
 			continue
 		}
 
-		mnt, err := s.buildMount(ctx, p, types.ServiceVolumeConfig{
+		mnt, err := buildMount(p, types.ServiceVolumeConfig{
 			Type:     types.VolumeTypeBind,
 			Source:   definedSecret.File,
 			Target:   target,
@@ -1039,7 +1039,7 @@ func isWindowsAbs(p string) bool {
 	return false
 }
 
-func (s *composeService) buildMount(ctx context.Context, project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
+func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
 	source := volume.Source
 	// on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock.
 	// do not replace these with  filepath.Abs(source) that will include a default drive.
@@ -1060,10 +1060,7 @@ func (s *composeService) buildMount(ctx context.Context, project types.Project,
 		}
 	}
 
-	bind, vol, tmpfs, err := s.buildMountOptions(ctx, volume)
-	if err != nil {
-		return mount.Mount{}, err
-	}
+	bind, vol, tmpfs := buildMountOptions(project, volume)
 
 	volume.Target = path.Clean(volume.Target)
 
@@ -1083,7 +1080,7 @@ func (s *composeService) buildMount(ctx context.Context, project types.Project,
 	}, nil
 }
 
-func (s *composeService) buildMountOptions(ctx context.Context, volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, error) {
+func buildMountOptions(project types.Project, volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) {
 	switch volume.Type {
 	case "bind":
 		if volume.Volume != nil {
@@ -1092,8 +1089,7 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
 		if volume.Tmpfs != nil {
 			logrus.Warnf("mount of type `bind` should not define `tmpfs` option")
 		}
-		option, err := s.buildBindOption(ctx, volume.Bind)
-		return option, nil, nil, err
+		return buildBindOption(volume.Bind), nil, nil
 	case "volume":
 		if volume.Bind != nil {
 			logrus.Warnf("mount of type `volume` should not define `bind` option")
@@ -1101,7 +1097,12 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
 		if volume.Tmpfs != nil {
 			logrus.Warnf("mount of type `volume` should not define `tmpfs` option")
 		}
-		return nil, buildVolumeOptions(volume.Volume), nil, nil
+		if v, ok := project.Volumes[volume.Source]; ok && v.DriverOpts["o"] == types.VolumeTypeBind {
+			return buildBindOption(&types.ServiceVolumeBind{
+				CreateHostPath: true,
+			}), nil, nil
+		}
+		return nil, buildVolumeOptions(volume.Volume), nil
 	case "tmpfs":
 		if volume.Bind != nil {
 			logrus.Warnf("mount of type `tmpfs` should not define `bind` option")
@@ -1109,30 +1110,19 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
 		if volume.Volume != nil {
 			logrus.Warnf("mount of type `tmpfs` should not define `volume` option")
 		}
-		return nil, nil, buildTmpfsOptions(volume.Tmpfs), nil
+		return nil, nil, buildTmpfsOptions(volume.Tmpfs)
 	}
-	return nil, nil, nil, nil
+	return nil, nil, nil
 }
 
-func (s *composeService) buildBindOption(ctx context.Context, bind *types.ServiceVolumeBind) (*mount.BindOptions, error) {
+func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
 	if bind == nil {
-		return nil, nil
-	}
-
-	propagation := bind.Propagation
-	isWindowsContainer, err := s.isWindowsContainer(ctx)
-	if err != nil {
-		return nil, err
-	}
-	if propagation == "" && !isWindowsContainer {
-		propagation = types.PropagationRPrivate
+		return nil
 	}
-
 	return &mount.BindOptions{
-		Propagation:      mount.Propagation(propagation),
-		CreateMountpoint: bind.CreateHostPath,
+		Propagation: mount.Propagation(bind.Propagation),
 		// NonRecursive: false, FIXME missing from model ?
-	}, nil
+	}
 }
 
 func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {


Test patch:
diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go
index 58a9d5fc5..e25ccd9f6 100644
--- a/pkg/compose/convergence_test.go
+++ b/pkg/compose/convergence_test.go
@@ -28,6 +28,7 @@ import (
 	containerType "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/network"
+	"github.com/docker/go-connections/nat"
 	"go.uber.org/mock/gomock"
 	"gotest.tools/v3/assert"
 
@@ -300,10 +301,17 @@ func TestCreateMobyContainer(t *testing.T) {
 			},
 		}
 
-		apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool {
-			v := x.(*containerType.HostConfig)
-			return v.NetworkMode == "b-moby-name"
-		}), gomock.Eq(
+		var falseBool bool
+		apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq(
+			&containerType.HostConfig{
+				PortBindings: nat.PortMap{},
+				ExtraHosts:   []string{},
+				Tmpfs:        map[string]string{},
+				Resources: containerType.Resources{
+					OomKillDisable: &falseBool,
+				},
+				NetworkMode: "b-moby-name",
+			}), gomock.Eq(
 			&network.NetworkingConfig{
 				EndpointsConfig: map[string]*network.EndpointSettings{
 					"b-moby-name": {
@@ -382,10 +390,17 @@ func TestCreateMobyContainer(t *testing.T) {
 			},
 		}
 
-		apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool {
-			v := x.(*containerType.HostConfig)
-			return v.NetworkMode == "b-moby-name"
-		}), gomock.Eq(
+		var falseBool bool
+		apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq(
+			&containerType.HostConfig{
+				PortBindings: nat.PortMap{},
+				ExtraHosts:   []string{},
+				Tmpfs:        map[string]string{},
+				Resources: containerType.Resources{
+					OomKillDisable: &falseBool,
+				},
+				NetworkMode: "b-moby-name",
+			}), gomock.Eq(
 			&network.NetworkingConfig{
 				EndpointsConfig: map[string]*network.EndpointSettings{
 					"a-moby-name": {
diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go
index c732ba447..34d5f59c4 100644
--- a/pkg/compose/create_test.go
+++ b/pkg/compose/create_test.go
@@ -17,7 +17,6 @@
 package compose
 
 import (
-	"context"
 	"os"
 	"path/filepath"
 	"sort"
@@ -42,8 +41,7 @@ func TestBuildBindMount(t *testing.T) {
 		Source: "",
 		Target: "/data",
 	}
-	s := composeService{}
-	mount, err := s.buildMount(context.TODO(), project, volume)
+	mount, err := buildMount(project, volume)
 	assert.NilError(t, err)
 	assert.Assert(t, filepath.IsAbs(mount.Source))
 	_, err = os.Stat(mount.Source)
@@ -58,8 +56,7 @@ func TestBuildNamedPipeMount(t *testing.T) {
 		Source: "\\\\.\\pipe\\docker_engine_windows",
 		Target: "\\\\.\\pipe\\docker_engine",
 	}
-	s := composeService{}
-	mount, err := s.buildMount(context.TODO(), project, volume)
+	mount, err := buildMount(project, volume)
 	assert.NilError(t, err)
 	assert.Equal(t, mount.Type, mountTypes.TypeNamedPipe)
 }
@@ -78,8 +75,7 @@ func TestBuildVolumeMount(t *testing.T) {
 		Source: "myVolume",
 		Target: "/data",
 	}
-	s := composeService{}
-	mount, err := s.buildMount(context.TODO(), project, volume)
+	mount, err := buildMount(project, volume)
 	assert.NilError(t, err)
 	assert.Equal(t, mount.Source, "myProject_myVolume")
 	assert.Equal(t, mount.Type, mountTypes.TypeVolume)
@@ -156,8 +152,8 @@ func TestBuildContainerMountOptions(t *testing.T) {
 			},
 		},
 	}
-	s := composeService{}
-	mounts, err := s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], moby.ImageInspect{}, inherit)
+
+	mounts, err := buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
 	sort.Slice(mounts, func(i, j int) bool {
 		return mounts[i].Target < mounts[j].Target
 	})
@@ -169,7 +165,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
 	assert.Equal(t, mounts[2].VolumeOptions.Subpath, "etc")
 	assert.Equal(t, mounts[3].Target, "\\\\.\\pipe\\docker_engine")
 
-	mounts, err = s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], moby.ImageInspect{}, inherit)
+	mounts, err = buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
 	sort.Slice(mounts, func(i, j int) bool {
 		return mounts[i].Target < mounts[j].Target
 	})