From 506dbf6ec55ddc9c8adb4f1d70c3ddfcbda2164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Wed, 11 Feb 2026 13:43:39 +0100 Subject: [PATCH 1/3] container/opts: Add bind-create-mountpoint mount option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for the `bind-create-mountpoint` option in bind mounts, which instructs the daemon to create the mountpoint directory inside the container if it doesn't exist. This allows to replace the legacy `-v /src/dir:/dst` with the `--mount`. Usage: --mount type=bind,src=/host/path,dst=/container/path,bind-create-mountpoint --mount type=bind,src=/host/path,dst=/container/path,bind-create-mountpoint=true Signed-off-by: Paweł Gronowski --- e2e/container/run_test.go | 31 +++++++++++++++++++++++++++ opts/mount.go | 7 ++++++- opts/mount_test.go | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/e2e/container/run_test.go b/e2e/container/run_test.go index d52230e23899..af92b82b3c6f 100644 --- a/e2e/container/run_test.go +++ b/e2e/container/run_test.go @@ -5,6 +5,7 @@ import ( "io" "math/rand" "os/exec" + "path/filepath" "strings" "syscall" "testing" @@ -160,6 +161,36 @@ func TestMountSubvolume(t *testing.T) { } } +func TestMountBindCreateMountpoint(t *testing.T) { + environment.SkipIfDaemonNotLinux(t) + + for _, tc := range []struct { + name string + value string + expectSuccess bool + }{ + {name: "flag only", value: "bind-create-mountpoint", expectSuccess: true}, + {name: "true", value: "bind-create-mountpoint=true", expectSuccess: true}, + {name: "1", value: "bind-create-mountpoint=1", expectSuccess: true}, + {name: "false", value: "bind-create-mountpoint=false", expectSuccess: false}, + {name: "0", value: "bind-create-mountpoint=0", expectSuccess: false}, + } { + t.Run(tc.name, func(t *testing.T) { + srcPath := filepath.Join("/tmp", t.Name(), "does", "not", "exist") + result := icmd.RunCommand("docker", "run", "--rm", + "--mount", "type=bind,src="+srcPath+",dst=/mnt,"+tc.value, + fixtures.AlpineImage, "cat", "/proc/mounts") + if tc.expectSuccess { + result.Assert(t, icmd.Success) + assert.Check(t, is.Contains(result.Stdout(), "/mnt")) + } else { + result.Assert(t, icmd.Expected{ExitCode: 125}) + assert.Check(t, is.Contains(result.Stderr(), srcPath)) + } + }) + } +} + func TestProcessTermination(t *testing.T) { var out bytes.Buffer cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage, diff --git a/opts/mount.go b/opts/mount.go index 2f252aaca5fe..e44bd7e3bd1c 100644 --- a/opts/mount.go +++ b/opts/mount.go @@ -57,7 +57,7 @@ func (m *MountOpt) Set(value string) error { if !hasValue { switch key { - case "readonly", "ro", "volume-nocopy", "bind-nonrecursive": + case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-mountpoint": // boolean values default: return fmt.Errorf("invalid field '%s' must be a key=value pair", field) @@ -102,6 +102,11 @@ func (m *MountOpt) Set(value string) error { default: return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val) } + case "bind-create-mountpoint": + ensureBindOptions(&mount).CreateMountpoint, err = parseBoolValue(key, val, hasValue) + if err != nil { + return err + } case "volume-subpath": ensureVolumeOptions(&mount).Subpath = val case "volume-nocopy": diff --git a/opts/mount_test.go b/opts/mount_test.go index 9520948b9af1..9072d922f2df 100644 --- a/opts/mount_test.go +++ b/opts/mount_test.go @@ -475,6 +475,50 @@ func TestMountOptSetTmpfsNoError(t *testing.T) { } } +func TestMountOptSetBindCreateMountpoint(t *testing.T) { + tests := []struct { + value string + exp bool + expErr string + }{ + {value: "", exp: false}, + {value: "bind-create-mountpoint", exp: true}, + {value: "bind-create-mountpoint=", expErr: `invalid value for 'bind-create-mountpoint': value is empty`}, + {value: "bind-create-mountpoint= true", expErr: `invalid value for 'bind-create-mountpoint' in 'bind-create-mountpoint= true': value should not have whitespace`}, + {value: "bind-create-mountpoint=no", expErr: `invalid value for 'bind-create-mountpoint': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`}, + {value: "bind-create-mountpoint=1", exp: true}, + {value: "bind-create-mountpoint=true", exp: true}, + {value: "bind-create-mountpoint=0", exp: false}, + {value: "bind-create-mountpoint=false", exp: false}, + } + + for _, tc := range tests { + name := tc.value + if name == "" { + name = "not set" + } + t.Run(name, func(t *testing.T) { + val := "type=bind,target=/foo,source=/foo" + if tc.value != "" { + val += "," + tc.value + } + var m MountOpt + err := m.Set(val) + if tc.expErr != "" { + assert.Error(t, err, tc.expErr) + return + } + assert.NilError(t, err) + if tc.value == "" { + assert.Check(t, is.Nil(m.values[0].BindOptions)) + } else { + assert.Check(t, m.values[0].BindOptions != nil) + assert.Check(t, is.Equal(m.values[0].BindOptions.CreateMountpoint, tc.exp)) + } + }) + } +} + func TestMountOptSetBindRecursive(t *testing.T) { t.Run("enabled", func(t *testing.T) { var m MountOpt From 3f16b45c2c357d84ebad1560032ae90ed9c9ba83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Thu, 12 Feb 2026 16:43:13 +0100 Subject: [PATCH 2/3] opts/mount: Rename to bind-create-host-path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- e2e/container/run_test.go | 10 +++++----- opts/mount.go | 4 ++-- opts/mount_test.go | 18 +++++++++--------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/e2e/container/run_test.go b/e2e/container/run_test.go index af92b82b3c6f..6b35127b47a3 100644 --- a/e2e/container/run_test.go +++ b/e2e/container/run_test.go @@ -169,11 +169,11 @@ func TestMountBindCreateMountpoint(t *testing.T) { value string expectSuccess bool }{ - {name: "flag only", value: "bind-create-mountpoint", expectSuccess: true}, - {name: "true", value: "bind-create-mountpoint=true", expectSuccess: true}, - {name: "1", value: "bind-create-mountpoint=1", expectSuccess: true}, - {name: "false", value: "bind-create-mountpoint=false", expectSuccess: false}, - {name: "0", value: "bind-create-mountpoint=0", expectSuccess: false}, + {name: "flag only", value: "bind-create-src", expectSuccess: true}, + {name: "true", value: "bind-create-src=true", expectSuccess: true}, + {name: "1", value: "bind-create-src=1", expectSuccess: true}, + {name: "false", value: "bind-create-src=false", expectSuccess: false}, + {name: "0", value: "bind-create-src=0", expectSuccess: false}, } { t.Run(tc.name, func(t *testing.T) { srcPath := filepath.Join("/tmp", t.Name(), "does", "not", "exist") diff --git a/opts/mount.go b/opts/mount.go index e44bd7e3bd1c..60ca36665d27 100644 --- a/opts/mount.go +++ b/opts/mount.go @@ -57,7 +57,7 @@ func (m *MountOpt) Set(value string) error { if !hasValue { switch key { - case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-mountpoint": + case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-src": // boolean values default: return fmt.Errorf("invalid field '%s' must be a key=value pair", field) @@ -102,7 +102,7 @@ func (m *MountOpt) Set(value string) error { default: return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val) } - case "bind-create-mountpoint": + case "bind-create-src": ensureBindOptions(&mount).CreateMountpoint, err = parseBoolValue(key, val, hasValue) if err != nil { return err diff --git a/opts/mount_test.go b/opts/mount_test.go index 9072d922f2df..2014c98d55c4 100644 --- a/opts/mount_test.go +++ b/opts/mount_test.go @@ -475,21 +475,21 @@ func TestMountOptSetTmpfsNoError(t *testing.T) { } } -func TestMountOptSetBindCreateMountpoint(t *testing.T) { +func TestMountOptSetBindCreateSrc(t *testing.T) { tests := []struct { value string exp bool expErr string }{ {value: "", exp: false}, - {value: "bind-create-mountpoint", exp: true}, - {value: "bind-create-mountpoint=", expErr: `invalid value for 'bind-create-mountpoint': value is empty`}, - {value: "bind-create-mountpoint= true", expErr: `invalid value for 'bind-create-mountpoint' in 'bind-create-mountpoint= true': value should not have whitespace`}, - {value: "bind-create-mountpoint=no", expErr: `invalid value for 'bind-create-mountpoint': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`}, - {value: "bind-create-mountpoint=1", exp: true}, - {value: "bind-create-mountpoint=true", exp: true}, - {value: "bind-create-mountpoint=0", exp: false}, - {value: "bind-create-mountpoint=false", exp: false}, + {value: "bind-create-src", exp: true}, + {value: "bind-create-src=", expErr: `invalid value for 'bind-create-src': value is empty`}, + {value: "bind-create-src= true", expErr: `invalid value for 'bind-create-src' in 'bind-create-src= true': value should not have whitespace`}, + {value: "bind-create-src=no", expErr: `invalid value for 'bind-create-src': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`}, + {value: "bind-create-src=1", exp: true}, + {value: "bind-create-src=true", exp: true}, + {value: "bind-create-src=0", exp: false}, + {value: "bind-create-src=false", exp: false}, } for _, tc := range tests { From 302c5cc10915e23be4fb7ba4491b77d42a0184ac Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 12 Feb 2026 16:43:31 +0100 Subject: [PATCH 3/3] docs/service: Document bind-create-host-path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- docs/reference/commandline/service_create.md | 19 ++++++++++++++++++- docs/reference/run.md | 8 ++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 691e2db2e05f..ddb6a3a0832c 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -457,6 +457,22 @@ The following options can only be used for bind mounts (`type=bind`): When the option is not specified, the default behavior corresponds to setting enabled. + + bind-create-src + + By default, bind mounts require the source path to exist on the daemon host. This is a significant difference + from the -v flag, which creates the source path if it doesn't exist.
+
+ Set bind-create-src to create the source path on the daemon host if it doesn't exist.
+
+ A value is optional:
+
+
    +
  • true or 1: Create path on the daemon host if it doesn't exist.
  • +
  • false or 0: Default behavior. Produces an error if the source path doesn't exist on the daemon host.
  • +
+ + ##### Bind propagation @@ -591,7 +607,8 @@ or `--volume` flag for `docker run`, with some important exceptions: - When you use `--mount` with `type=bind`, the host-path must refer to an *existing* path on the host. The path will not be created for you and the service will fail - with an error if the path does not exist. + with an error if the path does not exist. You can use `bind-create-src` to + create the host path if it doesn't exist. - The `--mount` flag does not allow you to relabel a volume with `Z` or `z` flags, which are used for `selinux` labeling. diff --git a/docs/reference/run.md b/docs/reference/run.md index 9c8b8baf0f25..b49d30cbc5b2 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -252,6 +252,14 @@ two paths. The `source` path is the location on the host that you want to bind mount into the container. The `target` path is the mount destination inside the container. +By default, bind mounts require the source path to exist on the daemon host. If the +source path doesn't exist, an error is returned. To create the source path on +the daemon host if it doesn't exist, use the `bind-create-src` option: + +```console +$ docker run -it --mount type=bind,source=[PATH],target=[PATH],bind-create-src busybox +``` + Bind mounts are read-write by default, meaning that you can both read and write files to and from the mounted location from the container. Changes that you make, such as adding or editing files, are reflected on the host filesystem: