fix: respect DOKKU_LIB_HOST_ROOT for storage bind mounts
When Dokku runs in Docker with a shared socket, create/chown still use DOKKU_LIB_ROOT inside the container, but Docker -v flags for storage:exec and deploy must use DOKKU_LIB_HOST_ROOT so the daemon mounts the host data volume instead of the real host's /var/lib/dokku. Closes #9126.
This commit is contained in:
@@ -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.
|
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
|
```shell
|
||||||
dokku storage:ensure-directory node-js-app
|
dokku storage:ensure-directory node-js-app
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
- 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`.
|
- Image build cache is set to the data dir + `/home/dokku`.
|
||||||
- The docker socket is mounted into container.
|
- 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func buildDockerExecArgs(entry *storage.Entry, input StorageExecInput) ([]string
|
|||||||
args = append(args, "--user", user)
|
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, input.Image)
|
||||||
args = append(args, cmd...)
|
args = append(args, cmd...)
|
||||||
return args, nil
|
return args, nil
|
||||||
|
|||||||
@@ -116,3 +116,25 @@ func TestBuildDockerExecArgsMountsHostPath(t *testing.T) {
|
|||||||
t.Fatalf("expected -v /srv/demo:/data, got: %s", joined)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -190,6 +191,26 @@ func GetStorageDirectory() string {
|
|||||||
return fmt.Sprintf("%s/data/storage", dokkuLibRoot)
|
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
|
// storageDirScriptNames lists the sudo helpers shipped in the plugin's bin
|
||||||
// directory. Each one takes a storage entry basename and builds the path
|
// 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
|
// under $DOKKU_LIB_ROOT/data/storage itself, so no caller can point them
|
||||||
|
|||||||
@@ -107,3 +107,46 @@ func TestGetStorageDirectory(t *testing.T) {
|
|||||||
dir := GetStorageDirectory()
|
dir := GetStorageDirectory()
|
||||||
Expect(dir).To(ContainSubstring("data/storage"))
|
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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ var appSchedulerFor = common.GetAppScheduler
|
|||||||
// buildDockerVFlag formats the Docker -v argument for a docker-local
|
// buildDockerVFlag formats the Docker -v argument for a docker-local
|
||||||
// attachment.
|
// attachment.
|
||||||
func buildDockerVFlag(entry *Entry, attachment *Attachment) string {
|
func buildDockerVFlag(entry *Entry, attachment *Attachment) string {
|
||||||
host := entry.HostPath
|
host := HostPathForDocker(entry.HostPath)
|
||||||
container := attachment.ContainerPath
|
container := attachment.ContainerPath
|
||||||
if host == "" || container == "" {
|
if host == "" || container == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ func TestBuildDockerVFlagVolumeOptions(t *testing.T) {
|
|||||||
Expect(roOpts).To(Equal("-v /host:/container:ro,noexec,nosuid"))
|
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) {
|
func TestTriggerDockerArgsRejectsSchedulerMismatch(t *testing.T) {
|
||||||
RegisterTestingT(t)
|
RegisterTestingT(t)
|
||||||
root := withTempLibRoot(t)
|
root := withTempLibRoot(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user