diff --git a/docs/advanced-usage/persistent-storage.md b/docs/advanced-usage/persistent-storage.md index d7884d758..12acdbca8 100644 --- a/docs/advanced-usage/persistent-storage.md +++ b/docs/advanced-usage/persistent-storage.md @@ -88,6 +88,8 @@ Each entry mirrors the underlying attachment. `readonly` (boolean) and `volume_o A storage directory can be created with the `storage:ensure-directory` command. This command will create a subdirectory in the recommended `/var/lib/dokku/data/storage` path - created during Dokku installation - and prepare it for use with an app. +When Dokku itself runs in a container with a shared Docker socket, default named storage is still created under `DOKKU_LIB_ROOT` (typically `/var/lib/dokku`) inside the Dokku container. Docker `-v` mounts for `storage:exec` and app deploys rewrite that path to `DOKKU_LIB_HOST_ROOT` so the host Docker daemon binds the directory from the host-visible data volume. See the [Docker installation notes](/docs/getting-started/install/docker.md) for setting `DOKKU_LIB_HOST_ROOT`. + ```shell dokku storage:ensure-directory node-js-app ``` diff --git a/docs/getting-started/install/docker.md b/docs/getting-started/install/docker.md index 86f5f3b0c..225612648 100644 --- a/docs/getting-started/install/docker.md +++ b/docs/getting-started/install/docker.md @@ -87,8 +87,9 @@ Dokku is run in the following configuration: - Data within the container is stored on the host within the `/var/lib/dokku` directory. - Image build cache is set to the data dir + `/home/dokku`. - The docker socket is mounted into container. +- `DOKKU_HOST_ROOT` and `DOKKU_LIB_HOST_ROOT` tell Dokku how the host Docker daemon sees those same trees when creating bind mounts (app repos and plugin data such as named storage). -Application repositories, plugin config, as well as plugin data are persisted to disk within the specified host directory for `/var/lib/dokku`. +Application repositories, plugin config, as well as plugin data are persisted to disk within the specified host directory for `/var/lib/dokku`. `DOKKU_LIB_HOST_ROOT` must point at that host path's `var/lib/dokku` subtree so Docker `-v` mounts resolve under the data volume rather than under the host's own `/var/lib/dokku`. Other docker container options can also be used when running Dokku, though the specific outcome will depend upon the specified options. For example, the Dokku container's nginx port can be bound to a specific host ip by specifying `--publish $HOST_IP:8080:80`, where `$HOST_IP` is the IP desired. Please see the [docker container run documentation](https://docs.docker.com/engine/reference/commandline/run/) for further explanation for various docker arguments. diff --git a/plugins/scheduler-docker-local/storage_exec.go b/plugins/scheduler-docker-local/storage_exec.go index 4db397e29..c2d8629dd 100644 --- a/plugins/scheduler-docker-local/storage_exec.go +++ b/plugins/scheduler-docker-local/storage_exec.go @@ -137,7 +137,7 @@ func buildDockerExecArgs(entry *storage.Entry, input StorageExecInput) ([]string args = append(args, "--user", user) } - args = append(args, "-v", fmt.Sprintf("%s:/data", entry.HostPath)) + args = append(args, "-v", fmt.Sprintf("%s:/data", storage.HostPathForDocker(entry.HostPath))) args = append(args, input.Image) args = append(args, cmd...) return args, nil diff --git a/plugins/scheduler-docker-local/storage_exec_test.go b/plugins/scheduler-docker-local/storage_exec_test.go index 110bab0eb..79556de30 100644 --- a/plugins/scheduler-docker-local/storage_exec_test.go +++ b/plugins/scheduler-docker-local/storage_exec_test.go @@ -116,3 +116,25 @@ func TestBuildDockerExecArgsMountsHostPath(t *testing.T) { t.Fatalf("expected -v /srv/demo:/data, got: %s", joined) } } + +func TestBuildDockerExecArgsRewritesLibHostRoot(t *testing.T) { + t.Setenv("DOKKU_LIB_ROOT", "/var/lib/dokku") + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + + entry := &storage.Entry{ + Name: "demo-data", + Scheduler: storage.SchedulerDockerLocal, + HostPath: "/var/lib/dokku/data/storage/demo-data", + } + args, err := buildDockerExecArgs(entry, StorageExecInput{ + Image: "alpine:3", + Command: []string{"ls"}, + }) + if err != nil { + t.Fatalf("err: %v", err) + } + joined := strings.Join(args, " ") + if !strings.Contains(joined, "-v /host/var/lib/dokku/data/storage/demo-data:/data") { + t.Fatalf("expected rewritten -v host path, got: %s", joined) + } +} diff --git a/plugins/storage/storage.go b/plugins/storage/storage.go index 18fc19447..987f583f0 100644 --- a/plugins/storage/storage.go +++ b/plugins/storage/storage.go @@ -3,6 +3,7 @@ package storage import ( "errors" "fmt" + "os" "path/filepath" "regexp" "strings" @@ -190,6 +191,26 @@ func GetStorageDirectory() string { return fmt.Sprintf("%s/data/storage", dokkuLibRoot) } +// HostPathForDocker rewrites a path under DOKKU_LIB_ROOT to the +// DOKKU_LIB_HOST_ROOT view used by the host Docker daemon. Named +// volumes and paths outside DOKKU_LIB_ROOT are returned unchanged. +// +// When Dokku runs in a container with a shared Docker socket, filesystem +// operations must use DOKKU_LIB_ROOT (visible inside the Dokku container) +// while Docker -v host paths must use DOKKU_LIB_HOST_ROOT (visible to the +// daemon on the real host). +func HostPathForDocker(path string) string { + libRoot := common.GetenvWithDefault("DOKKU_LIB_ROOT", "/var/lib/dokku") + libHost := os.Getenv("DOKKU_LIB_HOST_ROOT") + if libHost == "" || libHost == libRoot { + return path + } + if path == libRoot || strings.HasPrefix(path, libRoot+"/") { + return libHost + strings.TrimPrefix(path, libRoot) + } + return path +} + // storageDirScriptNames lists the sudo helpers shipped in the plugin's bin // directory. Each one takes a storage entry basename and builds the path // under $DOKKU_LIB_ROOT/data/storage itself, so no caller can point them diff --git a/plugins/storage/storage_test.go b/plugins/storage/storage_test.go index 871bdf53a..854040016 100644 --- a/plugins/storage/storage_test.go +++ b/plugins/storage/storage_test.go @@ -107,3 +107,46 @@ func TestGetStorageDirectory(t *testing.T) { dir := GetStorageDirectory() Expect(dir).To(ContainSubstring("data/storage")) } + +func TestHostPathForDocker(t *testing.T) { + RegisterTestingT(t) + + t.Setenv("DOKKU_LIB_ROOT", "/var/lib/dokku") + + t.Run("unset host root leaves path unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "") + Expect(HostPathForDocker("/var/lib/dokku/data/storage/foo")).To(Equal("/var/lib/dokku/data/storage/foo")) + }) + + t.Run("equal host root leaves path unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/var/lib/dokku") + Expect(HostPathForDocker("/var/lib/dokku/data/storage/foo")).To(Equal("/var/lib/dokku/data/storage/foo")) + }) + + t.Run("rewrites paths under lib root", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("/var/lib/dokku/data/storage/foo")).To(Equal("/host/var/lib/dokku/data/storage/foo")) + Expect(HostPathForDocker("/var/lib/dokku")).To(Equal("/host/var/lib/dokku")) + }) + + t.Run("leaves named volumes unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("myvolume")).To(Equal("myvolume")) + }) + + t.Run("leaves custom absolute paths unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("/srv/demo")).To(Equal("/srv/demo")) + }) + + t.Run("does not false-match similar prefixes", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("/var/lib/dokku-extra/data")).To(Equal("/var/lib/dokku-extra/data")) + }) +} diff --git a/plugins/storage/triggers.go b/plugins/storage/triggers.go index 144986331..d35a767aa 100644 --- a/plugins/storage/triggers.go +++ b/plugins/storage/triggers.go @@ -308,7 +308,7 @@ var appSchedulerFor = common.GetAppScheduler // buildDockerVFlag formats the Docker -v argument for a docker-local // attachment. func buildDockerVFlag(entry *Entry, attachment *Attachment) string { - host := entry.HostPath + host := HostPathForDocker(entry.HostPath) container := attachment.ContainerPath if host == "" || container == "" { return "" diff --git a/plugins/storage/triggers_test.go b/plugins/storage/triggers_test.go index 29b4b311e..f8913be75 100644 --- a/plugins/storage/triggers_test.go +++ b/plugins/storage/triggers_test.go @@ -61,6 +61,17 @@ func TestBuildDockerVFlagVolumeOptions(t *testing.T) { Expect(roOpts).To(Equal("-v /host:/container:ro,noexec,nosuid")) } +func TestBuildDockerVFlagRewritesLibHostRoot(t *testing.T) { + RegisterTestingT(t) + + t.Setenv("DOKKU_LIB_ROOT", "/var/lib/dokku") + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + + entry := &Entry{HostPath: "/var/lib/dokku/data/storage/demo"} + flag := buildDockerVFlag(entry, &Attachment{ContainerPath: "/app/data"}) + Expect(flag).To(Equal("-v /host/var/lib/dokku/data/storage/demo:/app/data")) +} + func TestTriggerDockerArgsRejectsSchedulerMismatch(t *testing.T) { RegisterTestingT(t) root := withTempLibRoot(t)