Skip to content

Persistent Storage

New

Introduced in 0.5.0. Named storage entries introduced in 0.38.0.

The preferred method to attach persistent storage to a Dokku-managed container is the Dokku storage plugin.

storage:annotations:report [<name>] [<flag>]                         # Displays annotations for one or more storage entries
storage:annotations:set <name> <key> [<value>]                       # Set or clear a single annotation on a storage entry
storage:create <name> [<path>] [flags]                               # Register a named storage entry
storage:destroy <name> [--force] [--destroy-host-dir]                # Remove a named storage entry (must be unmounted from every app first)
storage:ensure-directory [--chown option] <directory>                # [DEPRECATED] use storage:create instead
storage:exec <name> [-- <cmd>...]                                    # Run a command (or shell) in a temporary container that mounts the entry
storage:info <name> [--format text|json]                             # Show details for one storage entry
storage:labels:report [<name>] [<flag>]                              # Displays labels for one or more storage entries
storage:labels:set <name> <key> [<value>]                            # Set or clear a single label on a storage entry
storage:list <app> [--format text|json]                              # List bind mounts for an app's container(s) (legacy host:container view)
storage:list-entries [--scheduler s] [--format text|json]            # List registered storage entries
storage:migrate [<app>|--all]                                        # Re-run the legacy -v to attachment migration for an app
storage:mount [--replace] <app> <host-dir:container-dir>... [flags]  # Create or replace bind mounts
storage:report [<app>|--global] [<flag>]                             # Displays a storage report for one or more apps
storage:set <name> <property> [<value>]                              # Update a storage entry in place
storage:unmount [--all] <app> [<host-dir:container-dir>...] [flags]  # Remove one or all bind mounts
storage:wait <name>                                                  # Wait for a storage entry's PVC to be bound (k3s)

A storage entry is the source of truth for the underlying volume - a host directory on docker-local, or a PersistentVolumeClaim on k3s. Multiple apps can mount the same entry, and an attachment carries the per-app details (container path, phases, subpath, readonly, process type). Names are globally unique across the install and must be DNS-1123 labels (lowercase letters, digits, dashes) of 45 characters or less so they can be used verbatim as Helm release and PVC names.

The legacy storage:mount <app> <host>:<container> form continues to work on docker-local. On a k3s app it is rejected; create a named entry with storage:create --scheduler k3s and mount it instead. Existing colon-form mounts are migrated automatically the first time the new code runs - they show up as legacy-<hash> entries in storage:list-entries.

The storage plugin supports the following mount points:

  • explicit paths that exist on the host (docker-local)
  • docker volumes (docker-local)
  • PersistentVolumeClaims provisioned via a StorageClass (k3s)
  • hostPath-backed PVs (k3s)

Usage

Listing persistent storage

Persistent storage bind mounts are specified on a per-app basis, and can be listed with the storage:list command:

dokku storage:list node-js-app
-----> node-js-app volume bind-mounts:
       /var/lib/dokku/data/storage/node-js-app:/app/storage

The output format can also be set to json for programmatic access:

dokku storage:list node-js-app --format json
[
  {
    "entry_name": "node-js-app",
    "host_path": "/var/lib/dokku/data/storage/node-js-app",
    "container_path": "/app/storage"
  }
]

Each entry mirrors the underlying attachment. readonly (boolean) and volume_options (string) reflect Attachment.Readonly and Attachment.VolumeOptions directly and are only present when set, so external tooling can drift-detect attachments against the raw attachment fields. For example, a mount created with --volume-options noexec,nosuid --volume-readonly renders as:

[
  {
    "entry_name": "node-js-data",
    "host_path": "/var/lib/dokku/data/storage/node-js-data",
    "container_path": "/app/storage",
    "readonly": true,
    "volume_options": "noexec,nosuid"
  }
]

Creating storage directories

New

Introduced in 0.25.5

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.

dokku storage:ensure-directory node-js-app
-----> Ensuring /var/lib/dokku/data/storage/node-js-app exists
       Setting directory ownership to 32767:32767
       Directory ready for mounting

By default, permissions are set for usage with Herokuish buildpacks. These permissions can be changed via the --chown option according to the following table:

  • --chown herokuish (default): Use 32767:32767 as the folder permissions.
    • This is used for apps deployed with Buildpacks via Herokuish.
  • --chown heroku: Use 1000:1000 as the folder permissions.
    • This is used for apps deployed with Cloud Native Buildpacks using the heroku/builder builder.
  • --chown paketo: Use 2000:2000 as the folder permissions.
    • This is used for apps deployed with Cloud Native Buildpacks using the cloudfoundry/cnb or paketo builders.
  • --chown root: Use 0:0 as the folder permissions.
    • This is used for containers that run their processes as root, as is typical for most Dockerfile or Docker image deploys.
  • --chown false: Skips the chown call.
  • --chown <uid>: Use <uid>:<uid> as the folder permissions, where <uid> is a custom numeric user/group id.
    • This is used for containers that run their processes as a uid/gid that doesn't correspond to any of the above named options.

Users deploying via Dockerfile will want to specify --chown false and manually chown the created directory if the user and/or group id of the runnning process in the deployed container do not correspond to any of the above options.

The --chown flag - whether on storage:create or storage:ensure-directory - only manages the default /var/lib/dokku/data/storage/<name> location. If a custom <path> is passed to storage:create, the chown call is refused and the operator must chown the path themselves.

Warning

Failing to set the correct directory ownership may result in issues in persisting files written to the mounted storage directory.

Setting directory permissions

New

Introduced in 0.38.27

Where --chown states who owns the host directory, --mode states its permission bits. It takes a 3 or 4 digit octal mode, and is a --mode flag on storage:create and a mode property on storage:set:

dokku storage:create node-js-data --mode 0777
dokku storage:set node-js-data mode 0770

Omitting the value clears the mode, leaving the directory's permissions alone on subsequent runs:

dokku storage:set node-js-data mode

Without a mode, a newly created directory keeps the 0755 default and a pre-existing directory keeps whatever permissions it already had. The value is stored on the entry and re-applied every time storage:create or storage:set runs against it, so a declarative caller converges the directory by re-running the same command rather than reaching for chmod over SSH. The mode is shown by storage:info:

dokku storage:info node-js-data
-----> Storage entry node-js-data
       Scheduler:        docker-local
       Host path:        /var/lib/dokku/data/storage/node-js-data
       Mode:             0777

The mode is applied to the directory itself and does not recurse into its contents. Like --chown, it is docker-local only and only manages the default /var/lib/dokku/data/storage/<name> location - it is refused for k3s entries and for entries created with a custom <path>. That refusal also covers migrated legacy-* entries, whose host paths come from the original colon-form mount rather than the default location.

Updating a storage entry

New

The property form is introduced in 0.38.27. Prior versions used flags, which still work but emit a deprecation warning.

An existing entry is edited with storage:set, which takes a property and a value. Omitting the value unsets the property, restoring whatever the entry defaults to:

dokku storage:set node-js-data chown herokuish
dokku storage:set node-js-data chown

The following properties can be set:

Property Description Unsetting it means
chown Ownership preset or numeric uid for the host directory no chown is performed
mode Octal permissions for the host directory permissions are left alone
namespace Namespace holding the PVC (k3s) the default namespace
reclaim-policy Whether the underlying volume survives storage:destroy Retain
size PVC size (k3s) rejected, since k3s entries require a size
access-mode PVC access mode (k3s) rejected, see below
storage-class-name PVC storage class (k3s) rejected, see below

access-mode and storage-class-name cannot be changed on an entry that already exists, because Kubernetes cannot apply either to a bound PVC. Both a different value and an empty one are refused, since clearing is equally a change:

dokku storage:set node-js-data access-mode ReadWriteMany
 !     storage:set cannot change access-mode in place; recreate the entry

Setting chown or mode on a docker-local entry applies the change to the host directory immediately. Every other property is a metadata write, and k3s entries re-apply their helm release so the cluster picks the change up.

The older flag form - dokku storage:set node-js-data --mode 0770 - continues to work and warns. It gained unset semantics too, so --mode "" clears the mode the same way omitting the positional value does.

Annotations and labels

New

Introduced in 0.38.27

Annotations and labels are attached to a storage entry one key at a time, matching the scheduler-k3s equivalents. On k3s they propagate to both the PersistentVolumeClaim and the PersistentVolume, so backup tools like Velero and Longhorn can find the volume.

dokku storage:annotations:set node-js-data backup.velero.io/backup-volumes node-js-data
dokku storage:labels:set node-js-data app.kubernetes.io/part-of billing

Keys may contain /, as the Kubernetes-style keys above do, and are stored verbatim. To clear a single key, omit the value. Other keys are left untouched, so a declarative caller does not need to re-send the whole set on every call:

dokku storage:annotations:set node-js-data backup.velero.io/backup-volumes

Configured annotations and labels can be inspected with the matching report commands. Without an entry name they cover every registered entry:

dokku storage:annotations:report
dokku storage:annotations:report node-js-data
dokku storage:labels:report node-js-data
=====> node-js-data annotations information
       Annotation backup.velero.io/backup-volumes: node-js-data

JSON output emits the keys flat, and a single value can be read directly with a flag of the form --storage-annotations.<key> (or --storage-labels.<key>), which requires an entry name:

dokku storage:annotations:report node-js-data --format json
dokku storage:annotations:report node-js-data --storage-annotations.backup.velero.io/backup-volumes

storage:create still accepts repeatable --annotation key=value and --label key=value flags for setting the initial set at creation time. The same flags on storage:set are deprecated in favor of these commands, because they replace the entire map rather than a single key.

Mounting storage into apps

Dokku supports mounting both explicit host paths as well as docker volumes via the storage:mount command. This takes two arguments, an app name and a host-path:container-path or docker-volume:container-path combination.

# mount the directory into your container's /app/storage directory, relative to the container root (/)
# explicit host paths _must_ exist prior to usage.
dokku storage:mount node-js-app /var/lib/dokku/data/storage/node-js-app:/app/storage

# mount the docker volume into your container's /app/storage directory, relative to the container root (/)
# docker volumes _must_ exist prior to usage.
dokku storage:mount node-js-app some-docker-volume:/app/storage

In the first example, Dokku will then mount the shared contents of /var/lib/dokku/data/storage/node-js-app to /app/storage inside the container. The mount point is not relative to your app's working directory, and is instead relative to the root (/) of the container. Mounts are only available for containers created via run and by the deploy process, and not during the build process. In addition, the host path is never auto-created by either Dokku or Docker, and should be an explicit path, not one relative to the current working directory.

Info

If the /storage path within the container had pre-existing content, the container files will be over-written. This may be an issue for users that create assets at build time but then mount a directory at the same place during runtime. Files are not merged.

For named storage entries, additional Docker mount options can be passed via --volume-options. The value is a comma-separated mount-options string stored verbatim on the attachment and rendered into the -v flag at deploy time. This is useful for SELinux labels (Z, z) or hardening flags (noexec,nosuid):

dokku storage:create node-js-data
dokku storage:mount node-js-app node-js-data --container-dir /app/storage --volume-options Z

When combined with --volume-readonly, the rendered options become ro,<volume-options> - for example, --volume-options noexec,nosuid --volume-readonly renders as :ro,noexec,nosuid.

Re-running storage:mount against a named entry with the same --container-dir and --process-type updates the existing attachment's mount-time attributes (--phase, --volume-subpath, --volume-readonly, --volume-chown, --volume-options) in place rather than appending a duplicate. This is the idempotent equivalent of storage:set for entries, and lets declarative tooling change a mount-time attribute without an unmount-then-remount dance that would briefly drop the volume from storage:report. Mount-time fields are rewritten wholesale, not merged - omitting a flag on a re-mount clears any previously-set value. The legacy host:container[:opts] form still rejects duplicates with Mount path already exists..

Once persistent storage is mounted, the app requires a restart. See the process scaling documentation for more information.

dokku ps:restart app-name

Replacing the entire mount set

New

Introduced in 0.38.28

The form above mounts one entry at a time, so matching an app's mounts to a declared set means reading storage:list, computing the difference in both directions and issuing one storage:mount or storage:unmount per addition and removal - with a failure partway through leaving the app holding a mixture of the two sets. The --replace flag writes the whole set in a single call, taking name:container-dir pairs instead of a single entry and a --container-dir flag.

dokku storage:mount --replace node-js-app node-js-data:/app/storage node-js-cache:/cache

Anything previously mounted and not named in the call is unmounted. Each pair takes the same optional third field as the legacy colon form, so ro and other mount options can vary between mounts:

dokku storage:mount --replace node-js-app node-js-data:/app/storage:Z node-js-cache:/cache:ro,noexec

A pair whose first field starts with / is a legacy host path rather than an entry name, and registers its legacy-<hash> entry the same way the colon form does. Any other first field must name a registered entry, so a mistyped name is rejected rather than taken for a docker volume. A docker volume already mounted through the colon form is named here by the legacy-<hash> entry that storage:list-entries shows.

The remaining mount-time flags scope the whole call rather than a single pair, so every mount in the replacement shares them:

dokku storage:mount --replace node-js-app node-js-data:/app/storage --phase deploy --volume-subpath uploads --volume-chown herokuish

This means --volume-subpath and --volume-chown cannot differ between mounts declared in one call. Mounts needing different values for either are declared under a different --process-type, or set individually with the single-entry form.

--process-type scopes the replacement the same way it scopes a single mount. Omitting it replaces the _default_ process type, leaving mounts scoped to a named process type in place:

dokku storage:mount --replace node-js-app node-js-data:/app/storage
dokku storage:mount --replace node-js-app node-js-cache:/cache --process-type web

Every pair is parsed and validated before anything is written, so a rejected pair leaves the stored mounts untouched. A pair without a container directory, a container directory that is not absolute, an entry that is not registered, an entry whose scheduler does not match the app's, and a container directory named more than once in one call are all rejected.

Note

An empty pair list is rejected rather than treated as a request to unmount everything, so a generated list that expands to nothing cannot silently drop an app's storage. Use storage:unmount --all for that.

As with a single mount, the app requires a restart afterward.

Unmounting storage

If an app no longer requires a mounted volume or directory, the storage:unmount command can be called. This takes the same arguments as the storage:mount command, an app name and a host-path:container-path or docker-volume:container-path combination, or a registered entry name.

# unmount the directory from your container's /app/storage directory, relative to the container root (/)
dokku storage:unmount node-js-app /var/lib/dokku/data/storage/node-js-app:/app/storage

# unmount the docker volume from your container's /app/storage directory, relative to the container root (/)
dokku storage:unmount node-js-app some-docker-volume:/app/storage

New

Removing more than one attachment at a time is introduced in 0.38.28

More than one attachment can be removed in a single call, naming each by entry name or by the colon form. Every argument is resolved against the app's attachments before any of them is removed, so an argument naming an attachment the app does not have removes none of them:

dokku storage:unmount node-js-app node-js-data node-js-cache

An entry mounted at several container directories is disambiguated with --container-dir, which therefore applies only when a single entry is named. Naming the container directory in the argument itself works with any number of attachments:

dokku storage:unmount node-js-app node-js-data --container-dir /app/storage
dokku storage:unmount node-js-app node-js-data:/app/storage node-js-data:/app/uploads

Removing every mount

New

Introduced in 0.38.28

The --all flag removes every attachment on an app. This is the empty case storage:mount --replace refuses to handle, and it takes no entry arguments:

dokku storage:unmount --all node-js-app

--process-type narrows what is removed to a single scope, filtering the same way it scopes a replacement. Omitting it removes every scope, and a scope that matches nothing is a no-op rather than an error, so the command is safe to re-run:

dokku storage:unmount --all node-js-app --process-type web

Once persistent storage is unmounted, the app requires a restart. See the process scaling documentation for more information.

dokku ps:restart app-name

Destroying storage entries

A named storage entry can be removed with the storage:destroy command. The entry must first be unmounted from every app that mounts it.

dokku storage:destroy rdmtest-entry

As the command is destructive - removing the registry entry and, depending on the scheduler and reclaim policy, the underlying volume - it will default to asking for confirmation before executing the removal.

 !     WARNING: Potentially Destructive Action
 !     This command will destroy storage entry rdmtest-entry.
 !     To proceed, type "rdmtest-entry"
-----> Storage entry rdmtest-entry destroyed

Info

rdmtest-entry

The confirmation may be avoided by providing the --force flag, which is useful for non-interactive or automated callers:

dokku storage:destroy rdmtest-entry --force

The global --force flag is also supported:

dokku --force storage:destroy rdmtest-entry

Removing the host directory

New

Introduced in 0.38.27

By default a docker-local entry's host directory survives storage:destroy - the entry is deregistered but the data stays on disk. The --destroy-host-dir flag removes the directory and everything in it:

dokku storage:destroy node-js-data --destroy-host-dir
 !     Storage entry node-js-data is backed by /var/lib/dokku/data/storage/node-js-data, which will be removed along with its contents.
 !     WARNING: Potentially Destructive Action
 !     This command will destroy storage entry node-js-data.
 !     To proceed, type "node-js-data"

The removal is recursive, so it succeeds whether or not the directory is empty. It is only permitted for entries at the default /var/lib/dokku/data/storage/<name> location; an entry created with a custom <path> is refused, and the operator removes the path themselves.

The same removal can be declared ahead of time with --reclaim-policy, which behaves for a docker-local host directory the way it behaves for a k3s PersistentVolume. An entry created with Delete has its host directory removed on storage:destroy without any extra flag, while Retain - the default when unset - keeps it:

dokku storage:create node-js-data --reclaim-policy Delete
dokku storage:destroy node-js-data --force

--destroy-host-dir is docker-local only. On a k3s entry the underlying volume is already governed by the reclaim policy recorded on the entry, so passing the flag is an error.

Displaying storage reports for an app

New

Introduced in 0.8.1

You can get a report about the app's storage status using the storage:report command:

dokku storage:report
=====> node-js-app storage information
       Storage build mounts:
       Storage deploy mounts: -v /var/lib/dokku/data/storage/node-js-app:/app/storage
       Storage run mounts:  -v /var/lib/dokku/data/storage/node-js-app:/app/storage
=====> python-sample storage information
       Storage build mounts:
       Storage deploy mounts:
       Storage run mounts:
=====> ruby-sample storage information
       Storage build mounts:
       Storage deploy mounts:
       Storage run mounts:

You can run the command for a specific app also.

dokku storage:report node-js-app
=====> node-js-app storage information
       Storage build mounts:
       Storage deploy mounts: -v /var/lib/dokku/data/storage/node-js-app:/app/storage
       Storage run mounts:  -v /var/lib/dokku/data/storage/node-js-app:/app/storage

You can pass flags which will output only the value of the specific information you want. For example:

dokku storage:report node-js-app --storage-deploy-mounts

In addition to the aggregated Storage build/deploy/run mounts: lines, the report emits one flat dotted key per attachment field, indexed from 1. The key shape is --storage-attachment.<index>.<field> for each of entry-name, host-path, container-path, phases, process-type, subpath, readonly, volume-options, and volume-chown. Fields render as empty strings when unset, and attachments are ordered by lex-sort of the index (so 10 sorts before 2):

dokku storage:create node-js-data
dokku storage:mount node-js-app node-js-data --container-dir /app/storage --volume-options Z --volume-chown herokuish --volume-subpath uploads
dokku storage:report node-js-app
=====> node-js-app storage information
       Storage attachment 1 container path:  /app/storage
       Storage attachment 1 entry name:      node-js-data
       Storage attachment 1 host path:       /var/lib/dokku/data/storage/node-js-data
       Storage attachment 1 phases:          deploy,run
       Storage attachment 1 process type:    _default_
       Storage attachment 1 readonly:        false
       Storage attachment 1 subpath:         uploads
       Storage attachment 1 volume chown:    herokuish
       Storage attachment 1 volume options:  Z
       Storage build mounts:
       Storage deploy mounts: -v /var/lib/dokku/data/storage/node-js-data:/app/storage:Z
       Storage run mounts:  -v /var/lib/dokku/data/storage/node-js-data:/app/storage:Z

The same keys are exposed in JSON output, both in the stripped (attachment.1.volume-options) and legacy (storage-attachment.1.volume-options) forms:

dokku storage:report node-js-app --format json | jq '. | with_entries(select(.key | startswith("attachment.")))'
{
  "attachment.1.container-path": "/app/storage",
  "attachment.1.entry-name": "node-js-data",
  "attachment.1.host-path": "/var/lib/dokku/data/storage/node-js-data",
  "attachment.1.phases": "deploy,run",
  "attachment.1.process-type": "_default_",
  "attachment.1.readonly": "false",
  "attachment.1.subpath": "uploads",
  "attachment.1.volume-chown": "herokuish",
  "attachment.1.volume-options": "Z"
}

Indices are assigned in stored order, so storage:mount --replace renumbers them: the attachments it leaves in place keep their relative order and the declared ones follow.

A single attachment field can be fetched directly via the info-flag form:

dokku storage:report node-js-app --storage-attachment.1.volume-options
Z

Use Cases

Sharing storage across deploys

Dokku is powered by Docker containers, which recommends in their best practices that containers be treated as ephemeral. In order to manage persistent storage for web apps, like user uploads or large binary assets like images, a directory outside the container should be mounted.

Shared storage between containers

When scaling your app, you may require a common location to access shared assets between containers, a storage mount can be used in this situation.

Shared storage across environments

Your app may be used in a cluster that requires containers or resources not running on the same host access your data. Mounting a shared file service (like S3FS or EFS) inside your container will give you great flexibility.

Backing up

Your app may have services that are running in memory and need to be backed up locally (like a key store). Mount a non ephemeral storage mount will allow backups that are not lost when the app is shut down.

Build phase

By default, Dokku will only bind storage mounts during the deploy and run phases. Under certain conditions, one might want to bind a storage mount during the build phase. This can be accomplished by using the docker-options plugin directly.

dokku docker-options:add node-js-app build "-v /tmp/python-test:/opt/test"

You cannot use mounted volumes during the build phase of a Dockerfile deploy. This is because Docker does not support volumes when executing docker build.

Warning

This can cause data loss if you bind a mount under /app in buildpack apps as herokuish will attempt to remove the original app path during the build phase.

App User and Persistent Storage file ownership (buildpack apps only)

New

Introduced in 0.7.1

By default, Dokku will execute your buildpack app processes as the herokuishuser user. You may override this by setting the DOKKU_APP_USER config variable.

Note

this user must exist in your herokuish image.

Additionally, the default docker-local scheduler that comes with Dokku will ensure your storage mounts are owned by either herokuishuser or the overridden value you have set in DOKKU_APP_USER. See the docker-local scheduler documentation docs for more information.

Properties

Internal properties

The following property is recorded internally by the storage plugin and is not exposed via storage:report:

Property Scope Description Source
legacy-mounts-migrated per-app Per-app marker recording that the app's legacy -v docker-options entries were drained into named storage entries plus attachments. Only set when at least one -v line was actually migrated; apps that have never had legacy mounts never receive this marker plugins/storage/migrate.go writes "true" after a successful drain