diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md
index 691e2db2e05f..54c82437c378 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-host-path |
+
+ By default, bind mounts require the source path to exist on the host. This is a significant difference
+ from the -v flag, which creates the source path if it doesn't exist.
+
+ Set bind-create-host-path to create the source path on the host if it doesn't exist.
+
+ A value is optional:
+
+
+ - true or 1: Create path on host if it doesn't exist.
+ - false or 0: Default behavior. If source path doesn't exist, an error will be reported.
+
+ |
+
##### Bind propagation
diff --git a/e2e/container/run_test.go b/e2e/container/run_test.go
index d52230e23899..b6cfb8626216 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-host-path", expectSuccess: true},
+ {name: "true", value: "bind-create-host-path=true", expectSuccess: true},
+ {name: "1", value: "bind-create-host-path=1", expectSuccess: true},
+ {name: "false", value: "bind-create-host-path=false", expectSuccess: false},
+ {name: "0", value: "bind-create-host-path=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 642f6500ab4c..077829be0382 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-host-path":
// 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-host-path":
+ 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..92870cfbb2e4 100644
--- a/opts/mount_test.go
+++ b/opts/mount_test.go
@@ -475,6 +475,50 @@ func TestMountOptSetTmpfsNoError(t *testing.T) {
}
}
+func TestMountOptSetBindCreateHostPath(t *testing.T) {
+ tests := []struct {
+ value string
+ exp bool
+ expErr string
+ }{
+ {value: "", exp: false},
+ {value: "bind-create-host-path", exp: true},
+ {value: "bind-create-host-path=", expErr: `invalid value for 'bind-create-host-path': value is empty`},
+ {value: "bind-create-host-path= true", expErr: `invalid value for 'bind-create-host-path' in 'bind-create-host-path= true': value should not have whitespace`},
+ {value: "bind-create-host-path=no", expErr: `invalid value for 'bind-create-host-path': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`},
+ {value: "bind-create-host-path=1", exp: true},
+ {value: "bind-create-host-path=true", exp: true},
+ {value: "bind-create-host-path=0", exp: false},
+ {value: "bind-create-host-path=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