* master: (546 commits)
chore(deps): bump gunicorn in /tests/apps/dockerfile-release
chore(deps): bump github.com/traefik/traefik/v2
chore(deps): bump gunicorn from 26.0.0 to 26.1.0 in /tests/apps/multi
chore(deps): bump gunicorn in /tests/apps/python-flask
chore(deps): bump pygments from 2.20.0 to 2.21.0 in /docs/_build
chore: bump herokuish to 0.11.16
chore: bump go modules
chore(deps): bump golang.org/x/crypto in /plugins/common
chore(deps): bump sqlparse in /tests/apps/dockerfile-release
chore(deps): bump helm.sh/helm/v3 in /plugins/scheduler-k3s
chore(deps): bump golang in /tests/apps/zombies-dockerfile-no-tini
chore(deps): bump golang in /tests/apps/go-fail-postdeploy
chore(deps): bump golang in /tests/apps/go-fail-predeploy
chore(deps): bump golang from 1.26.5 to 1.26.6 in /tests/apps/gogrpc
chore(deps): bump golang in /tests/apps/zombies-dockerfile-tini
chore(deps): bump google.golang.org/protobuf in /tests/apps/gogrpc
fix: retire cron containers past their active deadline
Release 0.38.27
fix: report traefik dns-provider env vars as global keys
fix: do not require a local image for k3s deploys
...
# Conflicts:
# common.mk
# contrib/dependencies.json
Cron containers were never reaped once they exceeded their active deadline, so a hung cron task ran indefinitely instead of being retired after 24 hours as documented. `cron:run` now also accepts a `--ttl-seconds` argument, matching the one `dokku run` already takes.
# History
## 0.38.27
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.27/bootstrap.sh
sudo DOKKU_TAG=v0.38.27 bash bootstrap.sh
```
### Bug Fixes
- #8933: @josegonzalez Do not require a local image for k3s deploys
- #8930: @josegonzalez Do not import an already-migrated ENV file
- #8919: @josegonzalez Regenerate vector config on app lifecycle changes
- #8917: @josegonzalez Apply app-label-alias to shipped events
### New Features
- #8934: @josegonzalez Report traefik dns-provider env vars as global keys
- #8920: @josegonzalez Add storage directory mode and removal flags
- #8914: @josegonzalez Add vector-cron-sink for scheduled cron task output
### Tests
- #8931: @dependabot[bot] chore(deps): bump djangorestframework from 3.17.2 to 3.18.0 in /tests/apps/dockerfile-release
- #8927: @dependabot[bot] chore(deps): bump djangorestframework from 3.17.1 to 3.17.2 in /tests/apps/dockerfile-release
- #8923: @dependabot[bot] chore(deps): bump python from 3.15.0b4-bookworm to 3.15.0rc1-bookworm in /tests/apps/dockerfile-release
### Dependencies
- #8926: @dependabot[bot] chore(deps): bump github.com/gliderlabs/sigil from 0.12.0 to 0.12.1 in /plugins/nginx-vhosts
- #8925: @dependabot[bot] chore(deps): bump packaging from 26.2 to 26.3 in /docs/_build
- #8921: @dokku-bot chore: bump pack to 0.40.9
- #8924: @dependabot[bot] chore(deps): bump soupsieve from 2.9.1 to 2.9.2 in /docs/_build
- #8922: @dependabot[bot] chore(deps): bump python from 3.15.0b4-alpine to 3.15.0rc1-alpine in /docs/_build
- #8915: @dokku-bot chore: bump herokuish to 0.11.15
The dynamic `dns-provider-*` properties were reported as `--traefik-dns-provider-<env_var>`, outside the `global`/`computed` namespace every other traefik property uses, so a consumer reading the json report could not tell which of the two a key represented. They are now reported as `--traefik-global-dns-provider-<env_var>`. The `basic-auth-password` property is a credential as well and is now masked in the default report output on the same terms as the dns provider values, meaning the raw value is returned only for `--format json` or when the flag is queried by name.
Kubernetes pulls the app image itself, so a k3s host is free to reap its local copy while the workload keeps running, which the `registry` plugin already does on its own. Deploys, restarts, `dokku run`, and in-cluster cron no longer assert that the image is present locally, falling back to the metadata recorded in the app's current Helm release. A `ps:restart` naming a single process type now rolls only that process type's pods rather than silently redeploying every one. Apps with an `app.json` postdeploy task still require the image locally, as that task runs on the Dokku host.
The preserved copy holds everything the legacy file was not allowed to import, which includes keys that had been `config:unset` on purpose, so a revoked secret can outlive its revocation in a file `dokku config:*` no longer reads. Nothing said the copy was the operator's to remove once reviewed.
Releases 0.38.0 through 0.38.25 recorded the ENV file migration and deliberately left the file at the old path, so 0.38.26 treating a leftover file as a hand-edit replayed the environment as it stood at that upgrade over every `config:set` and `config:unset` made since, reinstating variables and secrets that had been deliberately unset. A file found at the old path once its migration is on record is no longer imported: it is removed when it agrees with the current config, and is otherwise moved aside to `ENV.migrated` with the keys it disagrees on named in a warning. That warning now reaches the operator during an upgrade rather than being swallowed, which is why the overwrite went unreported.
`storage:set` now takes `<name> <property> [<value>]` like every other `:set` command, where omitting the value unsets the property. Previously it took flags and could not distinguish an empty value from an omitted one, so nothing it set could ever be cleared. The flag form keeps working and emits a deprecation warning.
Annotations and labels move to `storage:annotations:set`, `storage:annotations:report`, `storage:labels:set`, and `storage:labels:report`, matching the `scheduler-k3s` equivalents. These operate on a single key, so clearing one leaves the rest in place rather than replacing the whole map as the `--annotation` and `--label` flags do.
`storage:create` and `storage:set` accept a `--mode` flag that sets the octal permissions of a docker-local host directory, and `storage:destroy` accepts a `--destroy-host-dir` flag that removes the directory along with its contents. A docker-local entry also honors `--reclaim-policy Delete` at destroy time now, matching how that policy governs a k3s PersistentVolume. Both are limited to the default `/var/lib/dokku/data/storage/<name>` location, the same restriction `--chown` already carries. `storage:set` applies `--chown` and `--mode` to the directory rather than only recording them.
The generated vector config is a snapshot of the app list and their sink properties, but it was only ever written by `logs:set` and `logs:vector-start`. Renaming an app left a source filtering on a label no container carries and gave the new name no source at all, so the app kept a sink with nothing feeding it. Cloning produced the same result for the clone, and destroying an app left its source and sink behind, the latter still pointing at an endpoint decommissioned along with the app. The global relabel transform embeds app names directly in generated VRL, so a rename also left behind a branch naming an app that no longer existed. Every case was silent, and the only repair was an operator running `logs:vector-start`. The `post-app-clone-setup`, `post-app-rename-setup` and `post-delete` triggers now rewrite the config, warning rather than failing so that a config write cannot abort the app operation whose state it is derived from.
Closes#8918.
The alias was only ever used to build the `include_labels` filter on the generated vector source, while dokku labels containers with `com.dokku.app-name` unconditionally. Setting the property therefore pointed the source at a label no container carries, and log collection stopped without any error. The source now always filters the label dokku applies, and a generated remap renames the field on its way to the sink, which is what the property was documented to do. An app whose own alias differs from the global one gets a branch in the global pipeline, so a per-app value is honored even when the app ships through the global sink.
Closes#8916.
The `app-label-alias` test left the global property set, so every later test in the file generated a vector source filtering on a label that dokku never applies to a container, silently collecting nothing. Clearing it in teardown restores log collection for the rest of the file. The cron routing test now asserts against console sinks rather than files, since the sink an event reached is identifiable from vector's own output without depending on a writable host mount, and its task sleeps either side of its output because a cron container that exits immediately is removed before vector can attach to it.
# History
## 0.38.26
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.26/bootstrap.sh
sudo DOKKU_TAG=v0.38.26 bash bootstrap.sh
```
### Bug Fixes
- #8906: @josegonzalez Match docker options by shell word when removing
### New Features
- #8911: @josegonzalez Route wildcard domains through traefik on k3s
- #8909: @josegonzalez Support manually managed cert issuers on k3s
- #8903: @josegonzalez Support kernel sysctls on the k3s scheduler
- #8856: @youdie006 Add pre-parsed port_mappings to ports:report json
### Refactors
- #8863: @josegonzalez Move host-crontab generation into cron plugin
### Documentation
- #8908: @josegonzalez Document --global on scheduler-k3s report and set
- #8858: @bakatz Added instructions for restoring backups on different CPU architectures.
### Tests
- #8893: @dependabot[bot] chore(deps): bump django from 5.2.16 to 5.2.17 in /tests/apps/dockerfile-release
- #8891: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 293 to 294 in /tests/apps/php
- #8877: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.82.1 to 1.83.0 in /tests/apps/gogrpc
- #8874: @dependabot[bot] chore(deps): bump sass from 1.101.7 to 1.102.0 in /tests/apps/multi
- #8870: @dependabot[bot] chore(deps): bump sass from 1.101.6 to 1.101.7 in /tests/apps/multi
- #8866: @dependabot[bot] chore(deps): bump sass from 1.101.3 to 1.101.6 in /tests/apps/multi
- #8860: @dependabot[bot] chore(deps): bump setuptools from 78.1.1 to 83.0.0 in /tests/apps/dockerfile-release
- #8859: @dependabot[bot] chore(deps): bump immutable from 5.1.5 to 5.1.9 in /tests/apps/multi
- #8855: @dependabot[bot] chore(deps): bump sass from 1.101.0 to 1.101.3 in /tests/apps/multi
- #8857: @dependabot[bot] chore(deps): bump body-parser from 2.2.1 to 2.3.0 in /tests/apps/checks-root
- #8853: @dependabot[bot] chore(deps): bump python from 3.15.0b3-bookworm to 3.15.0b4-bookworm in /tests/apps/dockerfile-release
### Dependencies
- #8905: @dependabot[bot] chore(deps): bump oras.land/oras-go/v2 from 2.6.1 to 2.6.2 in /plugins/scheduler-k3s
- #8869: @dependabot[bot] chore(deps): bump github.com/mattn/go-isatty from 0.0.23 to 0.0.24 in /plugins/app-json
- #8887: @dependabot[bot] chore(deps): bump github.com/kedacore/keda/v2 from 2.20.1 to 2.20.2 in /plugins/scheduler-k3s
- #8883: @dokku-bot chore: bump docker-container-healthchecker to 0.16.0
- #8886: @dependabot[bot] chore(deps): update markdown requirement from <3.11,>=3.10.2 to >=3.10.3,<3.11 in /docs/_build
- #8892: @dependabot[bot] chore(deps): bump traefik from v3.7.9 to v3.7.10 in /plugins/traefik-vhosts
- #8885: @dokku-bot chore: bump dokku-update to 0.10.0
- #8884: @dokku-bot chore: bump procfile-util to 0.20.8
- #8882: @dokku-bot chore: bump docker-image-labeler to 0.10.0
- #8888: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.53 to 2.11.54 in /plugins/scheduler-k3s
- #8900: @dokku-bot chore: bump gliderlabs-sigil to 0.12.1
- #8899: @dokku-bot chore: bump herokuish to 0.11.14
- #8898: @dokku-bot chore: bump netrc to 0.11.1
- #8897: @dokku-bot chore: bump dokku-event-listener to 0.20.1
- #8896: @dokku-bot chore: bump sshcommand to 0.20.2
- #8895: @dokku-bot chore: bump lambda-builder to 0.9.4
- #8894: @dokku-bot chore: bump plugn to 0.17.1
- #8876: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.21.0 to 1.21.1 in /plugins/scheduler-k3s
- #8873: @dependabot[bot] chore(deps): bump traefik from v3.7.8 to v3.7.9 in /plugins/traefik-vhosts
- #8872: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.52 to 2.11.53 in /plugins/scheduler-k3s
- #8871: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.36.2 to 1.36.3 in /plugins/scheduler-k3s
- #8868: @dependabot[bot] chore(deps): bump k8s.io/kubectl from 0.36.2 to 0.36.3 in /plugins/scheduler-k3s
- #8867: @dependabot[bot] chore(deps): bump k8s.io/client-go from 0.36.2 to 0.36.3 in /plugins/scheduler-k3s
- #8865: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.38.0 to 1.39.0 in /plugins/scheduler-k3s
- #8864: @dependabot[bot] chore(deps): bump soupsieve from 2.9 to 2.9.1 in /docs/_build
- #8854: @dependabot[bot] chore(deps): bump python from 3.15.0b3-alpine to 3.15.0b4-alpine in /docs/_build
- #8852: @dependabot[bot] chore(deps): bump soupsieve from 2.8.4 to 2.9 in /docs/_build
- #8851: @dependabot[bot] chore(deps): bump mkdocs-material from 9.7.6 to 9.7.7 in /docs/_build
- #8850: @dependabot[bot] chore(deps): bump actions/setup-python from 6 to 7
### Other
- #8907: @josegonzalez fix: migrate env files before reading deprecated vars
- #8881: @josegonzalez Ignore minor and patch updates for github actions
Scheduled cron task output previously reached only the `dokku` user's cron mail, and could not be redirected because `app.json` rejects bare shell operators in a cron `command`. Setting `vector-cron-sink` on an app or globally routes that output to a dedicated sink instead, on both the `docker-local` and `k3s` schedulers, which keeps log destinations under operator control rather than in a deployed repository. Cron events carry `dokku_app` and `dokku_cron_id` fields so a sink can give each task its own destination. This also fixes a `k3s` bug where configuring a global `vector-sink` silently removed the vector prometheus exporter sink.
Traefik matches hosts exactly, so an app serving a wildcard domain under the `traefik` ingress class had a valid certificate but silently 404d on every request. Wildcard domains now render as a `HostRegexp` rule that matches a single label, the same semantics as a Kubernetes wildcard host, so both ingress classes behave the same. Those routes carry an explicit low priority so an exact domain on any app still wins over another app's wildcard, mirroring ingress-nginx.
The `cert-issuer-name` and `cert-issuer-kind` properties point an app's generated `Certificate` at a cert-manager issuer created outside of Dokku, allowing certificates to be issued through solvers the built-in letsencrypt integration cannot use, such as `dns01` for wildcard certificates. Setting an issuer enables https on its own, as a manually managed issuer has no email for Dokku to configure. An imported certificate still takes precedence, and `letsencrypt-server false` remains the single off switch. Dokku warns before a build starts when the referenced issuer is absent from the cluster, without blocking the deploy. Wildcard domains no longer collide with their apex domain when generating ingress names, and `letsencrypt-server` values are now validated when set rather than at deploy time.
The usage strings for `scheduler-k3s:report` and `scheduler-k3s:set` omitted the `--global` option, which is the only way to reach the scheduler-wide report since a bare `scheduler-k3s:report` iterates every app, and `:report` also omitted `--format stdout|json`. The command listing in the k3s documentation is resynced with the help output, which additionally restores flags that had been dropped from `scheduler-k3s:cluster:add`, `scheduler-k3s:cluster:list`, and `scheduler-k3s:initialize`.
The hand-written marker file was created as root, which the config-migrate-env
trigger could not overwrite when it ran under a different user. Draining once up
front records the migration through the same code path the assertion exercises.
Install steps run in alphabetical order of the enabled plugin directory, so `apps`, `builder`, and `checks` read an app's environment before the `config` plugin had moved the `ENV` file to its new location. The read came back empty, so their deprecated `DOKKU_*` variables were never migrated to the matching plugin property and were never unset, with nothing reported either way: `dokku config:show` kept listing the variable while the plugin behaved as though it were unset. The relocation now runs before any deprecated variable is read, whatever the install order, and each old file is removed as soon as it has been drained rather than on a later install, which also covers the global file that was never removed at all. A file that reappears at the old path can only have been written by hand, so it is merged in with a warning naming its keys instead of being discarded.
Options drained out of the pre-0.38.0 `DOCKER_OPTIONS_<PHASE>` files were copied verbatim rather than re-serialized the way `docker-options:add` stores them, so `docker-options:remove` compared the canonical string it builds against a stored value that could never match it and exited successfully without removing anything. Removal now matches stored options by shell word, and stored options are rewritten into the canonical form once on upgrade, which additionally splits an entry that carried several flags on a single line into one entry per flag so a single flag can be removed and so the readers that match on a flag prefix see one value per entry. The leftover `.migrated` sentinel drain is restored to running ahead of the global short-circuit that had made it unreachable, and the plugin's Go tests are added to the test target that had never run them.
Sysctls the kernel does not namespace, such as `vm.max_map_count`, cannot be set from a pod spec and previously had no answer beyond editing `/etc/sysctl.d` on each host by hand. `scheduler-k3s:node-sysctls:set` now applies them through a privileged daemonset, which reaches nodes joined later and reapplies after a reboot. Sysctls may be scoped to a node profile, with a profile scope inheriting the global values and overriding them on conflict so that every node is covered by exactly one daemonset. Clearing a sysctl stops dokku managing it but does not restore the previous value, which persists until the node reboots.
The `docker-local` scheduler supports `--sysctl` for free because docker options are passed verbatim to `docker run`, but the k3s scheduler silently dropped it. Namespaced sysctls now render into the pod's `securityContext.sysctls` for deployments, cron jobs, and one-off runs. A sysctl the kernel does not namespace fails the deploy instead of being dropped, since it cannot take effect within a pod regardless of what was requested.
Node profiles controlled how a node joined the cluster but left no trace on the node afterwards, so a profile could not be selected against with `kubectl`, a `nodeSelector`, or a node affinity rule. Nodes joined without a profile are left unlabeled, and the server node created by `scheduler-k3s:initialize` never carries the label since it does not pass through `scheduler-k3s:cluster:add`.
The server node created by `scheduler-k3s:initialize` had no way to receive kubelet arguments, unlike nodes joined through `scheduler-k3s:cluster:add` or configured via `scheduler-k3s:profiles:add`. This meant settings such as `allowed-unsafe-sysctls` were unreachable on a single-node install.
Host-crontab generation for `app.json` cron tasks now lives in the `cron` plugin, gated by a new `scheduler-uses-host-cron` trigger that the `docker-local` scheduler answers true while self-managed schedulers such as `k3s` answer false. This lets any host-cron scheduler participate in normal `app.json` cron without coupling to `scheduler-docker-local` or duplicating the crontab writer, while the `k3s` scheduler continues to manage its own in-cluster cron jobs.
Closes#8862.
The `issuer.yaml` chart template dereferenced `.Values.global.issuer.enabled` without guarding against the value being absent, which yaml serialization omitted for apps without a per-app email, causing a nil-pointer render error that broke every k3s web deploy.
The `letsencrypt-email-prod` and `letsencrypt-email-stag` properties can now be set per app in addition to globally, resolving app-level before the global value for the app's selected `letsencrypt-server`. An app that sets its own email renders a namespaced cert-manager `Issuer` using that email, while apps without an override continue to use the shared `ClusterIssuer` with the global email.
Values supplied through docker options, `--ttl-seconds`, and `-e` flowed into a Bash `eval` during build, deploy, and run, letting a low-privileged user execute arbitrary commands on the host as the dokku user. These arguments are now tokenized and passed through to the container verbatim, without shell expansion. A one-time migration repairs stored labels whose backticks were saved with a stray backslash so Traefik-style rules stay valid on the next deploy.
# History
## 0.38.24
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.24/bootstrap.sh
sudo DOKKU_TAG=v0.38.24 bash bootstrap.sh
```
### Documentation
- #8836: @josegonzalez Link herokuish buildpack references to buildpack management page
### Tests
- #8841: @dependabot[bot] chore(deps): bump ruby from 4.0.5 to 4.0.6 in /tests/apps/dockerfile-entrypoint
- #8843: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.82.0 to 1.82.1 in /tests/apps/gogrpc
### Dependencies
- #8845: @dependabot[bot] chore(deps): bump github.com/mattn/go-isatty from 0.0.22 to 0.0.23 in /plugins/app-json
- #8844: @dependabot[bot] chore(deps): bump github.com/melbahja/goph from 1.5.1 to 1.5.2 in /plugins/common
- #8842: @dependabot[bot] chore(deps): bump timberio/vector from 0.56.0-debian to 0.57.0-debian in /plugins/logs
- #8838: @dokku-bot chore: bump pack to 0.40.8
- #8846: @dependabot[bot] chore(deps): bump traefik from v3.7.7 to v3.7.8 in /plugins/traefik-vhosts
- #8839: @dependabot[bot] chore(deps): bump actions/setup-node from 6 to 7
- #8834: @dokku-bot chore: bump dokku-event-listener to 0.20.0
The Herokuish Buildpacks doc linked buildpack topics to anchors on the process management page that do not exist, so those links resolved to the wrong page. Point them at the corresponding sections of the buildpack management page and add that page to the sidebar so it is reachable.
# History
## 0.38.23
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.23/bootstrap.sh
sudo DOKKU_TAG=v0.38.23 bash bootstrap.sh
```
### Bug Fixes
- #8833: @josegonzalez Parse cert CN and subject on OpenSSL 3.x
### Tests
- #8825: @dependabot[bot] chore(deps): bump golang from 1.26.4 to 1.26.5 in /tests/apps/zombies-dockerfile-no-tini
- #8823: @dependabot[bot] chore(deps): bump golang from 1.26.4 to 1.26.5 in /tests/apps/zombies-dockerfile-tini
- #8820: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 292 to 293 in /tests/apps/php
- #8821: @dependabot[bot] chore(deps): bump golang from 1.26.4 to 1.26.5 in /tests/apps/go-fail-predeploy
- #8822: @dependabot[bot] chore(deps): bump golang from 1.26.4 to 1.26.5 in /tests/apps/gogrpc
- #8824: @dependabot[bot] chore(deps): bump golang from 1.26.4 to 1.26.5 in /tests/apps/go-fail-postdeploy
### Dependencies
- #8831: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.21.2 to 3.21.3 in /plugins/scheduler-k3s
- #8830: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.37.0 to 1.38.0 in /plugins/scheduler-k3s
- #8826: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.53.0 to 0.54.0 in /plugins/common
- #8827: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.20.3 to 1.21.0 in /plugins/scheduler-k3s
- #8828: @dependabot[bot] chore(deps): bump traefik from v3.7.6 to v3.7.7 in /plugins/traefik-vhosts
- #8829: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.24.0 to 1.0.0 in /plugins/scheduler-k3s
The `certs` plugin extracted a certificate's Common Name and formatted its subject using string assumptions that only held for pre-3.x OpenSSL output, so a certificate with only a Common Name and no Subject Alternative Name reported no hostnames from `certs:report` and was not recognized during nginx config generation, while the subject report retained the `subject=` prefix and used the wrong separators. Normalizing the subject with `-nameopt` before parsing makes the extraction version independent across OpenSSL and LibreSSL.
# History
## 0.38.22
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.22/bootstrap.sh
sudo DOKKU_TAG=v0.38.22 bash bootstrap.sh
```
### New Features
- #8805: @RichardDorian Allow custom values for chown
- #8810: @josegonzalez Expose whether a network was created by dokku
- #8807: @josegonzalez Allow replacing buildpack list atomically
- #8808: @josegonzalez Expose docker-options as structured lists in JSON report
- #8806: @josegonzalez Expose scheduler-k3s autoscaling-auth state for read-back
- #8801: @josegonzalez Add --format json support to plugin:list
### Refactors
- #8804: @josegonzalez Port bash :report subcommands to golang
### Documentation
- #8809: @josegonzalez Document nginx validate-config load_module override
### Tests
- #8803: @dependabot[bot] chore(deps): bump django from 5.2.15 to 5.2.16 in /tests/apps/dockerfile-release
### Dependencies
- #8816: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.21.0 to 0.22.0 in /plugins/common
- #8818: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.51 to 2.11.52 in /plugins/scheduler-k3s
- #8817: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.21.0 to 0.22.0 in /plugins/scheduler-docker-local
- #8819: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.21.0 to 0.22.0 in /plugins/scheduler-k3s
`network:list` and `network:info` now expose a `DokkuManaged` boolean derived from the `com.dokku.network-name` label that `network:create` applies, and `network:list` gains a `--dokku-managed` flag to restrict output to dokku-created networks. This lets tooling distinguish networks dokku created from Docker built-ins and networks created by other tooling such as compose.
The nginx deploy-time pre-validation runs `nginx -t` against a minimal wrapper that omits the global `load_module` directives, so a custom `nginx.conf.sigil` using a directive from a dynamically loaded module fails validation even though it is valid against the running server. Document overriding the `validate-config` template through the `nginx-app-template-source` trigger as the supported workaround.
Pin DOKKU_SYSTEM_USER and DOKKU_SYSTEM_GROUP to the current process user in test setup so PropertyListWrite succeeds in CI Docker where the dokku group does not exist.
Add parallel -list keys to docker-options:report --format json so export
tools can round-trip options without splitting space-joined strings.
Closes#8799
Add buildpacks:set --replace so callers can replace an app's complete ordered buildpack list in one command while preserving existing single-buildpack and --index behavior.
Closes#8802
Align autoscaling-auth:report with annotations and labels reporting so export tools can recover configured trigger auth via flat JSON keys and info flags, while stdout stays secret-safe unless --include-metadata is used.
Closes#8800
With every bash :report subcommand now ported to golang, the shared fn-report-parse-args, fn-report-emit-json, fn-report-filter-global and fn-report-validate-format helpers are no longer referenced and are removed.
The bash :report implementation for the nginx-vhosts plugin is replaced with a compiled golang binary that reuses the plugin's existing golang property getters, so every raw, global and computed key is unchanged while collection runs in parallel and json is marshalled directly. The global report keeps its existing behaviour of surfacing only the global keys.
The bash :report implementation for the scheduler-docker-local plugin is replaced with a compiled golang binary. The plugin already shipped golang code, so the report subcommand is added alongside its existing triggers binary, and the init-process and parallel-schedule-count helpers remain in bash for the deploy pipeline.
The bash :report implementation for the git plugin is replaced with a compiled golang binary. Property, computed and global keys are collected in parallel, while the sha and last-updated-at keys still shell out to git and stat the deploy branch ref so their values are unchanged. The non-report git helpers such as fn-git-cmd and the computed deploy-branch and keep-git-dir getters remain in place for the build pipeline.
The bash :report implementation for the certs plugin is replaced with a compiled golang binary. The certificate inspection getters run openssl and reproduce the existing field extraction, so the ssl report keys are unchanged, while collection now happens in parallel and json is marshalled directly. The now-unused fn-ssl-* display helpers are dropped, and fn-certs-set and fn-certs-remove remain for certs:add and certs:remove.
The bash :report implementations for the checks and domains plugins are replaced with a compiled golang binary that collects report keys in parallel and marshals json directly. The domains global report header now matches the shared renderer used by every other golang report, and its bats assertion is updated accordingly.
The bash :report implementations for the caddy, haproxy, openresty, and traefik proxy plugins are replaced with a compiled golang binary that collects report keys in parallel and marshals json directly. The traefik dns-provider values keep their masking behaviour, remaining hidden in the default stdout report while surfacing for --format json or an explicit flag query. These four plugins previously had no unit tests, so a bats suite covering the report matrix is added for each.
The bash :report implementations for the dockerfile, herokuish, lambda, nixpacks, pack, and railpack builders are replaced with a compiled golang binary that collects report keys in parallel and marshals json directly, avoiding the per-key subshell and jq forks that made the bash reports slow. The subcommands/report and root report trigger become symlinks to the compiled binary, while the non-report bash helpers each plugin still relies on are left in place.
Adds a `--format json` flag to `plugin:list` whose output includes each plugin's install source - for git-based third-party plugins, the git remote URL, the checked-out commit, and the followed branch - so the set of installed plugins can be reconstructed elsewhere.
Closes#8798.
# History
## 0.38.21
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.21/bootstrap.sh
sudo DOKKU_TAG=v0.38.21 bash bootstrap.sh
```
### New Features
- #8795: @josegonzalez Add --format json support to apps:list
### Tests
- #8788: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.81.1 to 1.82.0 in /tests/apps/gogrpc
- #8787: @dependabot[bot] chore(deps): bump golang.org/x/net from 0.51.0 to 0.55.0 in /tests/apps/gogrpc
### Dependencies
- #8781: @dependabot[bot] chore(deps): bump lucaslorentz/caddy-docker-proxy from 2.12 to 2.13 in /plugins/caddy-vhosts
- #8794: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.36.0 to 1.37.0 in /plugins/scheduler-k3s
- #8789: @dependabot[bot] chore(deps): bump pymdown-extensions from 11.0 to 11.0.1 in /docs/_build
- #8782: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.23.2 to 0.24.0 in /plugins/scheduler-k3s
- #8785: @dependabot[bot] chore(deps): bump traefik from v3.7.5 to v3.7.6 in /plugins/traefik-vhosts
- #8786: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.31.0 to 1.36.0 in /plugins/scheduler-k3s
- #8783: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.50 to 2.11.51 in /plugins/scheduler-k3s
- #8780: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.20.2 to 1.20.3 in /plugins/scheduler-k3s
- #8779: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.23.1 to 0.23.2 in /plugins/scheduler-k3s
### Other
- #8796: @josegonzalez Add --format json support to ps:scale
The `ps:scale` command now accepts a `--format` flag that defaults to `stdout` and can be set to `json` to emit the current formation as a JSON array of process type and quantity objects, matching the JSON output the `:report` subcommands already provide. The flag only applies when displaying the current formation; it is ignored when process types are supplied for scaling. When no scale has been set for the app, the JSON output is an empty array.
The `apps:list` command now accepts a `--format` flag that defaults to `stdout` and can be set to `json` to emit the app names as a JSON array, matching the JSON output the `:report` subcommands already provide. When no apps exist, the JSON output is an empty array.
# History
## 0.38.20
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.20/bootstrap.sh
sudo DOKKU_TAG=v0.38.20 bash bootstrap.sh
```
### Tests
- #8773: @dependabot[bot] chore(deps): bump python from 3.15.0b2-bookworm to 3.15.0b3-bookworm in /tests/apps/dockerfile-release
- #8756: @dependabot[bot] chore(deps): bump sass from 1.100.0 to 1.101.0 in /tests/apps/multi
- #8753: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 291 to 292 in /tests/apps/php
### Dependencies
- #8766: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.21.1 to 3.21.2 in /plugins/scheduler-k3s
- #8777: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.1 in /plugins/common
- #8778: @dependabot[bot] chore(deps): bump byjg/easy-haproxy from 6.1.0 to 6.1.1 in /plugins/haproxy-vhosts
- #8748: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.52.0 to 0.53.0 in /plugins/common
- #8745: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0 in /plugins/common
- #8774: @dependabot[bot] chore(deps): bump python from 3.15.0b2-alpine to 3.15.0b3-alpine in /docs/_build
- #8769: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.21.3 to 11.0 in /docs/_build
- #8775: @dependabot[bot] chore(deps): bump click from 8.4.1 to 8.4.2 in /docs/_build
- #8776: @dependabot[bot] chore(deps): bump byjg/easy-haproxy from 6.0.1 to 6.1.0 in /plugins/haproxy-vhosts
- #8772: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.42.0 to 1.42.1 in /plugins/buildpacks
- #8770: @dokku-bot chore: bump pack to 0.40.7
- #8767: @dependabot[bot] chore(deps): bump github.com/containerd/containerd from 1.7.32 to 1.7.33 in /plugins/scheduler-k3s
- #8765: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.0 in /plugins/scheduler-k3s
- #8764: @dependabot[bot] chore(deps): bump actions/checkout from 6 to 7
- #8762: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.36.1 to 1.36.2 in /plugins/scheduler-k3s
- #8758: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.0 in /plugins/buildpacks
- #8760: @dependabot[bot] chore(deps): bump k8s.io/apimachinery from 0.36.1 to 0.36.2 in /plugins/scheduler-k3s
- #8757: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.21.0 to 3.21.1 in /plugins/scheduler-k3s
- #8755: @dependabot[bot] chore(deps): bump traefik from v3.7.4 to v3.7.5 in /plugins/traefik-vhosts
- #8754: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.49 to 2.11.50 in /plugins/scheduler-k3s
- #8749: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.47 to 2.11.49 in /plugins/scheduler-k3s
- #8743: @dependabot[bot] chore(deps): bump beautifulsoup4 from 4.14.3 to 4.15.0 in /docs/_build
- #8744: @dependabot[bot] chore(deps): bump traefik from v3.7.3 to v3.7.4 in /plugins/traefik-vhosts
- #8747: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0 in /plugins/scheduler-k3s
- #8746: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0 in /plugins/scheduler-docker-local
The `helm.sh/helm/v3` v3.21.2 bump pulled `client-go` up to `v0.36.2`, which is incompatible with the pinned `controller-runtime v0.22.4`. Moving `controller-runtime` to `v0.24.1` (the first release targeting `client-go v0.36`) requires KEDA `v2.20.1`, allowing the old version pin to be removed.
# History
## 0.38.18
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.18/bootstrap.sh
sudo DOKKU_TAG=v0.38.18 bash bootstrap.sh
```
### Tests
- #8734: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/gogrpc
- #8731: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/go-fail-predeploy
- #8733: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/zombies-dockerfile-tini
- #8735: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/go-fail-postdeploy
- #8736: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/zombies-dockerfile-no-tini
- #8738: @dependabot[bot] chore(deps): bump python from 3.15.0b1-bookworm to 3.15.0b2-bookworm in /tests/apps/dockerfile-release
- #8730: @dependabot[bot] chore(deps): bump django from 5.2.14 to 5.2.15 in /tests/apps/dockerfile-release
### Dependencies
- #8727: @dependabot[bot] chore(deps): bump github.com/melbahja/goph from 1.5.0 to 1.5.1 in /plugins/common
- #8739: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.46 to 2.11.47 in /plugins/scheduler-k3s
- #8737: @dependabot[bot] chore(deps): bump dokku/openresty-docker-proxy from 0.12.0 to 0.12.1 in /plugins/openresty-vhosts
- #8732: @dependabot[bot] chore(deps): bump python from 3.15.0b1-alpine to 3.15.0b2-alpine in /docs/_build
- #8740: @dependabot[bot] chore(deps): bump timberio/vector from 0.55.0-debian to 0.56.0-debian in /plugins/logs
- #8741: @dependabot[bot] chore(deps): bump traefik from v3.7.1 to v3.7.3 in /plugins/traefik-vhosts
For an undeployed app, the additive diff covers every chart resource, so
the literal 'has been added' header appears once per resource - six times
in the python test app's chart. `assert_output_contains` defaults to
exact-count 1, which fails. Pass -1 (at least 1) to match the actual
semantics of the test.
`common.GetDeployingAppImageName` runs `VerifyImage` and errors out when
no image has been built for the app. A freshly-created app has no image
yet, so the preview could never render for never-deployed apps. Add an
`AllowMissingImage` flag to `BuildOptions` that lets BuildAppChart
substitute a placeholder image string in that case; CommandPreview sets
it. The deploy path still passes `BuildOptions{}`, so missing-image
remains a hard error there.
Without this, the preview build invented a fresh timestamp for
`Values.global.deployment_id` and the resulting diff showed every
Deployment changing its `app.kubernetes.io/version` annotation - even when
nothing semantically changed. CommandPreview now reads the deployment id
out of `release.Chart.Metadata.Version` and passes it through a new
`BuildOptions.OverrideDeploymentID` parameter so the previewed manifests
carry the same version label as the live release. The deploy path still
generates a fresh timestamp.
The previous commit added the dispatcher case but missed appending
`subcommands/preview` to the plugin's `SUBCOMMANDS` list, so no symlink
was created at install time and dokku reported the command as unknown.
Users can override the default 3-line surrounding context per change. Pass
`--context -1` to render the full resource around every change, matching
upstream `helm diff upgrade` behavior.
Adds a `scheduler-k3s:preview <app>` command that renders a unified diff
between the manifests currently stored in the live Helm release for an app
and the manifests that the next deploy would roll out. The diff is produced
entirely via the Helm Go SDK that scheduler-k3s already uses, with no
external `helm` binary or `helm-diff` plugin install required. The
underlying diff and parse logic is vendored from `databus23/helm-diff`
(Apache 2.0) into a new internal package, trimmed of three-way-merge,
release-ownership tracking, and the non-default output formatters.
The chart-construction half of `TriggerSchedulerDeploy` has been extracted
into a reusable `BuildAppChart` helper so deploy and preview share the
same chart materialization without duplicating template logic. Secret
values are redacted by default in preview output; `--show-secrets` and
`--show-secrets-decoded` flags match the upstream `helm diff` UX. Only the
main app helm release is compared in this iteration; auxiliary releases
for config vars, image-pull secrets, and TLS certificates are out of scope
and documented as such.
`common.TouchFile` opened files with `O_TRUNC`, so the call from `common.SshTask.Execute` against `~/.ssh/known_hosts` emptied the dokku user's previously trusted host keys before `goph.DefaultKnownHosts()` read them. Running `scheduler-k3s:cluster:add` without `--insecure-allow-unknown-hosts` then failed with `knownhosts: key is unknown` and left a zero-byte `known_hosts`. The function now matches `touch(1)` semantics: create the file if missing, otherwise leave its contents intact.
Kubernetes 1.20 deprecated the node-role.kubernetes.io/master label and replaced it with node-role.kubernetes.io/control-plane, and 1.24 dropped the legacy label entirely. The scheduler-k3s:cluster:add join path still filtered control-plane nodes with master=true, so on any k3s release built from Kubernetes 1.24+ the lookup matched zero nodes and aborted with "no nodes found in the cluster" before the worker could join.
Annotation and label values containing a newline previously round-tripped
as two adjacent `key: value` lines, and the second line then failed the
`SplitN(line, ": ", 2)` parse on read. Move the per-`(processType,
resourceType)` files to `PropertyMap*` storage so `\n` in values and `/`
in keys are preserved verbatim. An idempotent `TriggerInstall` migration
walks `--global` and every app, converting any legacy line-formatted file
in place by probing `PropertyMapGet` first and rewriting via
`PropertyListGet` only when the probe fails. Property names are
unchanged, so the annotations/labels report scanner and the
`reservedAnnotationPrefixes` filter keep working without modification.
Closes#8719.
PropertyMap tests went through SetPermissions, which looks up the `dokku` user/group; the fresh `golang:1.26.2` container used by `go-test-plugin-in-docker` has neither, so the chown failed with `unknown group dokku`. Override `DOKKU_SYSTEM_USER` and `DOKKU_SYSTEM_GROUP` to `root` in the property and chart-migration test setups so the chown targets an identity that exists everywhere. The config tests also called `plugn trigger post-config-update` without `PLUGIN_CORE_AVAILABLE_PATH` set, so the hook tried to source `/common/functions` and logged a noisy warning on every test; point the env var at the canonical `/var/lib/dokku/core-plugins/available` location.
Chart property names containing `/` failed because per-key flat-file storage interpreted the slash as a filesystem path separator, and any move to line-based storage would silently truncate multi-line values. A new `PropertyMap*` helper family in `common` persists each map as a single JSON file so both `/` in keys and `\n` in values round-trip losslessly. Chart overrides move to `chart-overrides.<chart>` and an idempotent `TriggerInstall` migration rewrites any legacy `chart.<chart>.<key>` files into the new layout. The deprecated `scheduler-k3s:set --global chart.*` form is rerouted through the same map storage so it does not silently orphan writes. Closes#8717.
The scheduler-k3s plugin exposes `annotations:set` and `labels:set` but no matching report subcommands, so scripts could not inspect the configured state for idempotent management without reading the property files directly. The new `scheduler-k3s:annotations:report` and `scheduler-k3s:labels:report` surface the configured entries with stdout, JSON, single-flag query, and `--process-type` / `--resource-type` filtering, mirroring the recently added `scheduler-k3s:charts:report`. The literal `GlobalProcessType` value `--global` is rendered as `global` in report keys to avoid leading dashes. The `scheduler-k3s:autoscaling-auth:report` command is updated alongside to loop over every app when no app and no `--global` flag is provided, matching the convention used by `scheduler-k3s:report`.
Helm chart overrides on the scheduler-k3s plugin were previously set
through the generic `scheduler-k3s:set --global chart.<chart>.<property>`
interface, which mixed chart-level configuration into the same command
that manages scheduler properties and offered no focused way to inspect
which overrides were configured. A dedicated `scheduler-k3s:charts:set`
sets and clears chart-specific helm values, and a complementary
`scheduler-k3s:charts:report` surfaces configured overrides per chart
with optional JSON output and single-field flag queries. The legacy form
on `scheduler-k3s:set` continues to work but now emits a deprecation
warning pointing users at the new subcommand.
storage:report gains a flat dotted key per attachment field (entry-name, host-path, container-path, phases, process-type, subpath, readonly, volume-options, volume-chown) under the --storage-attachment.<index>.<field> shape. Both stdout and JSON pick the keys up through the existing common.ReportSingleApp pipeline. Attachments that reference a missing storage entry emit a warning and are skipped, so the rest of the report still renders. Closes#8710.
Re-running `storage:mount <app> <entry> --container-dir <path>` against an existing `(entry, container_dir, process_type)` tuple now updates the attachment's mount-time fields in place instead of erroring with `already mounted`. Declarative tooling that wants to change `--volume-options`, `--volume-chown`, `--phase`, `--volume-subpath`, or `--volume-readonly` on an existing mount no longer has to unmount-then-remount, which briefly dropped the volume from `storage:report` and raced against any deploy that fired in the window. The legacy `host:container[:opts]` form keeps its strict `Mount path already exists.` failure.
The `storage:list <app> --format json` payload conflated the
attachment's `Readonly` flag and its `VolumeOptions` field into a
single derived `volume_options` string that rendered as `ro`,
`<options>`, or `ro,<options>` depending on which fields were set on
the underlying attachment. That shape is fine for the legacy
`host:container[:options]` text view but it leaves drift-detection
tooling unable to tell whether a `ro` token came from
`Attachment.Readonly == true` or from the operator setting
`Attachment.VolumeOptions = "ro"` directly. The JSON now exposes
`readonly` (boolean) and `volume_options` (string) as separate
omitempty keys populated straight from the attachment, and
`formatStorageListEntry` combines them at format time for the colon-
form text view. `ParseMountPath` was extended in lockstep so callers
of the legacy `host:container:opts` form no longer have to special-
case the `ro` token themselves.
The named-entry form of `storage:mount` had no way to set the attachment's `volume_options`, even though the legacy `host:container:opts` colon form parses options into the same field and every downstream consumer (the docker-args trigger and the storage:list/storage:report display path) already renders them. Tooling that declaratively re-applies attachments silently dropped options on every re-apply.
The apps plugin accepted `apps:set --global deploy-source` and
`apps:set --global deploy-source-metadata` even though nothing reads
those properties globally, accepted `apps:set <app> disable-autocreation`
even though only the global form is consulted by `maybeCreateApp`, and
never emitted `disable-autocreation` in `apps:report` for either scope.
Drop the global `deploy-source*` writes from `GlobalProperties`, reject
per-app `disable-autocreation` writes at the plugin level, and surface
`--app-global-disable-autocreation` in both the per-app and `--global`
reports so the property the runtime actually reads is round-trippable.
storage:create --chown invoked chown-storage-dir with the full host path, but the helper validates a basename and prepends the storage root itself, so every call failed with `Directory can only contain the following set of characters`. Pass the entry name and reject the combination of --chown with a non-default host path, since the helper only manages the default storage location.
# History
## 0.38.10
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.10/bootstrap.sh
sudo DOKKU_TAG=v0.38.10 bash bootstrap.sh
```
### New Features
- #8702: @josegonzalez Prompt for confirmation on storage:destroy
### Tests
- #8698: @dependabot[bot] chore(deps-dev): bump org.apache.maven.plugins:maven-dependency-plugin from 3.10.0 to 3.11.0 in /tests/apps/java
`storage:destroy` now prompts for confirmation before removing a named storage entry, matching the behavior of other destructive commands such as `apps:destroy` and `network:destroy`. The prompt can be skipped with the `--force` flag or the global `dokku --force` flag for non-interactive callers.
Emitting the restart policy as a `--restart` argument from a `docker-args-process-deploy` trigger exposed a latent gluing bug: proxy and builder triggers concatenated `$STDIN$output` with no separator, and the scheduler and builders appended `docker-args-process-*` output directly onto the older `docker-args-*` output, so adjacent arguments could merge into values like `--restart=on-failure:10--label`. A space is now inserted at both the trigger echo and the concatenation so arguments always stay separated.
`dokku ps:set <app> restart-policy` with no value erroneously returned `Invalid restart-policy specified` instead of unsetting the property like every other ps property. The restart policy is now managed as a normal app and global property surfaced through the `--ps-restart-policy`, `--ps-global-restart-policy`, and `--ps-computed-restart-policy` report flags, with the effective value applied at deploy time and existing values migrated on install. Because it is no longer stored as a Docker option it no longer appears in `docker-options:report`, and `--ps-restart-policy` now reports the raw value with the `on-failure:10` default available via `--ps-computed-restart-policy`.
Exposes raw, global, and computed report flags for every openresty per-app property and allows those properties to be set with `--global`, so external tooling can distinguish a property that was never set from one left at its built-in default. The computed value resolves the per-app value first, then the global value, then the default. Also corrects `client-header-timeout` which read the `client-body-timeout` key and sets the computed `client-max-body-size` default to `1m`.
Every Go-implemented plugin's `:report --format json` now emits keys without the redundant `<plugin>-` head segment, matching the shape bash plugins have always emitted. The CLI flag names and `:set` semantics are unchanged. For backwards compatibility during the 0.38.x patch series, the legacy `<plugin>-<property>` keys are emitted side-by-side with the new keys, and a future major release will drop the legacy keys. `common.ReportSingleApp` is refactored to accept a `ReportSingleAppInput` struct with a `Validate()` method so the input is checked before any work runs, which also catches the latent `"docker options"` reportType bug at the API boundary.
`dokku network:set --global bind-all-interfaces` (no value) wrote the literal string `false` to the property file instead of unsetting it, so external tooling could not distinguish "explicitly set to default" from "not set". Drop the empty-to-`false` coercion in `CommandSet` so the empty-value path reaches `PropertyDelete` like every other property, and remove the unconditional `TriggerPostCreate` write so new apps consult the global value before falling back to the `"false"` computed default.
Adds the missing raw/global/computed report keys for ten settable properties across the ps, builder, scheduler, proxy, openresty, nginx, and scheduler-k3s plugins so external tooling can verify drift through `:report --format json`. The scheduler-k3s `token` is masked as `*******` in default stdout output and only unmasked when the report is requested via `--format json` or when the flag is queried explicitly by name; the same explicit-query unmasking rule is extended to the existing traefik `dns-provider-<env_var>` keys.
The registry plugin accepted `registry:set <app> image-repo-template <value>` and persisted the per-app file, but only ever read the global value when rendering the image repository, so per-app overrides were silently dropped. The property now follows the same per-app to global to default fallback chain used by `push-on-release` and `server`, and `registry:report` exposes the raw per-app value alongside the existing global and computed flags. The `push-extra-tags` report flags were extended for the same reason: the runtime already honored per-app and global values, but the report only surfaced the raw per-app entry.
# History
## 0.38.7
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.7/bootstrap.sh
sudo DOKKU_TAG=v0.38.7 bash bootstrap.sh
```
### Security
- #8672: @josegonzalez Prevent host shell injection from app.json cron commands
### Bug Fixes
- #8669: @josegonzalez Recover deployed image from registry on local miss
- #8679: @josegonzalez Align ps/cron :report --global keys with plugin convention
- #8678: @josegonzalez Split openresty report keys into global and computed pairs
- #8676: @immanuwell Use SYSTEM for shfmt Darwin detection
### New Features
- #8677: @josegonzalez Warn on deprecated listen http2 in custom nginx templates
### Tests
- #8683: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 290 to 291 in /tests/apps/php
- #8671: @josegonzalez Qualify scheduler-k3s ingressroute kubectl lookups
### Dependencies
- #8682: @dependabot[bot] chore(deps): bump k8s.io/apimachinery from 0.36.0 to 0.36.1 in /plugins/scheduler-k3s
- #8681: @dependabot[bot] chore(deps): bump soupsieve from 2.8.3 to 2.8.4 in /docs/_build
### Other
- #8680: @josegonzalez Split more report global keys into raw and computed
The longest `ps:report` flag is now `--ps-computed-stop-timeout-seconds` (34 chars), which pushes the per-plugin stdout column width past the 31-char floor used by `common.ReportSingleApp`. The `(report) report` test's literal-string assertion for `Deployed:` needs three extra spaces to match the new alignment.
The `ps` and `cron` plugins emitted `:report --format json` keys that
skipped the `<plugin>-` prefix or `-global-` infix used by every other
plugin, forcing external consumers to special-case those names. Rename
the affected report flags so they match the `<plugin>-global-<property>`
and `<plugin>-computed-<property>` convention introduced in 0.38.0, and
add the previously missing `computed-` siblings to both the per-app and
`--global` reports so the shape lines up with caddy, haproxy, and
traefik.
Reverts the earlier triggerRestart change. Forwarding the numeric tag forces
release_and_deploy to look up `dokku/<app>:N` (the bare local name), which
was never tagged when image-repo overrides the default. `fn-verify-app-image`
then retag-after-pulls and the underlying image ID ends up with two tags,
which prevents scheduler-docker-local's image retire path from removing the
old numeric tag (it uses unforced `docker image remove`).
When `image-repo` is overridden, the registry plugin pushes under that name and
the bare local `dokku/<app>:N` tag is never created. `fn-verify-app-image` now
queries the `deployed-app-image-repo` trigger and pulls `${REMOTE_REPO}${DEPLOYED_REPO}:${TAG}`,
retagging it as the local bare reference so subsequent inspects resolve. Also
aligns the `storage:ensure-directory` deprecation test assertion with the
standardized `Deprecated:` prefix.
Adds an app-aware `fn-verify-app-image` helper that pulls the deployed image
from the configured registry when it is missing locally, gated on
push-on-release being enabled, honoring per-app `registry:login` credentials.
Every in-tree `verify_image` caller migrates to the wrapper and the bare
helper is marked deprecated. `config:set` now forwards the deployed image tag
to `release-and-deploy` so the release path no longer pins recovery on the
unrecoverable `:latest` tag.
Three plugins still leaked the built-in default through `<plugin>-global-<property>` after the broader raw/computed split in #8640. External tooling reading `:report --format json` for drift detection could not distinguish "set to default" from "never set" - after a `:set --global` followed by an unset, the JSON still reported the default value. The `<plugin>-global-<property>` keys now hold the raw stored value (empty when nothing has been set) and `<plugin>-computed-<property>` keys hold the effective value with per-app, global, and built-in default fallback. Every global property now has a computed sibling for shape consistency, including those whose default is empty. Internal callers in `scheduler-k3s` that previously read the default-applied global helpers now consume new `getComputed*` helpers, keeping deploy-time behavior unchanged.
The `openresty:report --format json` payload emitted bare property names for the four global-only properties (`image`, `letsencrypt-email`, `letsencrypt-server`, `allowed-letsencrypt-domains-func-base64`), while `hsts` already used the `global-`/`computed-` convention adopted by the other proxy plugins. External tooling consuming the report had to maintain a per-property naming map within a single plugin. The global flag map now emits `--openresty-global-<X>` (raw stored value, empty when unset) and `--openresty-computed-<X>` (effective value with the built-in default applied) for those four properties plus `hsts`, and the per-app map gains the same pair for the global-only properties. The compose template and the two non-report callers consume the `computed-` accessors so the runtime behavior is unchanged.
nginx 1.25.1 deprecated the `listen ... http2` parameter in favor of a standalone `http2 on;` directive. Apps that ship a custom `nginx.conf.sigil` forked from an old default still hardcode `listen ... ssl http2;` and trigger `nginx: [warn]` lines from `nginx -t`. Surface a deprecation warning during `nginx_build_config` whenever the active template is app- or plugin-supplied and still contains the deprecated parameter, matching the existing warnings for `DOKKU_APP_LISTENERS`, `NGINX_SSL_PORT`, and `NGINX_PORT`, and point users at the `HTTP2_DIRECTIVE_SUPPORTED` variable already used by the default template.
The docker-local scheduler wrote each app.json cron command verbatim into the dokku user's crontab, where cron's `bash -c` interpreted any shell metacharacters in the command on the host as the dokku user rather than inside the container. The crontab line is now `dokku cron:run <app> <cron-id>`, and the command is resolved from app.json and exec'd inside the container at run time. Commands containing shell operators are also rejected when app.json is validated at deploy time.
The Traefik 26.0.0 chart registers CRDs under both `traefik.containo.us` and `traefik.io`, so a bare `kubectl get ingressroute` resolves against the legacy group where no resources exist and returns NotFound. Querying `ingressroutes.traefik.io` targets the group Dokku's chart actually writes to.
# History
## 0.38.6
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.6/bootstrap.sh
sudo DOKKU_TAG=v0.38.6 bash bootstrap.sh
```
### Bug Fixes
- #8633: @Mordred Correctly redirect http traffic to https when using scheduler-k3s
- #8665: @josegonzalez Clarify byte-preserving behavior of config:set --encoded
- #8666: @josegonzalez Depend on cron | cron-daemon to allow alternatives
- #8638: @josegonzalez Support --global on cron:set
### Refactors
- #8668: @josegonzalez Hash scheduler-k3s cron-id label to fit Kubernetes' 63-byte cap
- #8664: @josegonzalez Convert filesystem migration markers to plugin properties
### Documentation
- #8645: @zenspider Removed outdated warning about dokku-update not being able to upgrade to specific versions.
### Tests
- #8667: @josegonzalez Rename logs:report vector key test and guard old keys
- #8662: @dependabot[bot] chore(deps): bump qs from 6.15.0 to 6.15.2 in /tests/apps/checks-root
- #8661: @dependabot[bot] chore(deps): bump sass from 1.99.0 to 1.100.0 in /tests/apps/multi
- #8658: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 288 to 290 in /tests/apps/php
- #8657: @dependabot[bot] chore(deps): bump slim/slim from 4.15.1 to 4.15.2 in /tests/apps/php
- #8654: @dependabot[bot] chore(deps): bump ruby from 4.0.4 to 4.0.5 in /tests/apps/dockerfile-entrypoint
- #8642: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.81.0 to 1.81.1 in /tests/apps/gogrpc
### Dependencies
- #8652: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.40.0 to 1.41.0 in /plugins/common
- #8663: @dokku-bot chore: bump herokuish to 0.11.13
- #8646: @dokku-bot chore: bump pack to 0.40.6
- #8659: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0 in /plugins/common
- #8660: @dependabot[bot] chore(deps): bump click from 8.4.0 to 8.4.1 in /docs/_build
- #8655: @dependabot[bot] chore(deps): bump github.com/containerd/containerd from 1.7.30 to 1.7.32 in /plugins/scheduler-k3s
- #8651: @dependabot[bot] chore(deps): bump click from 8.3.3 to 8.4.0 in /docs/_build
- #8649: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.40.0 to 1.41.0 in /plugins/buildpacks
- #8650: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.40.0 to 1.41.0 in /plugins/config
- #8648: @dependabot[bot] chore(deps): bump zipp from 3.23.1 to 4.1.0 in /docs/_build
- #8644: @dokku-bot chore: bump pack to 0.40.5
- #8641: @dependabot[bot] chore(deps): bump traefik from v3.7.0 to v3.7.1 in /plugins/traefik-vhosts
- #8634: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.21.2 to 10.21.3 in /docs/_build
### Other
- #8640: @josegonzalez Split report global keys into raw and computed
The `dokku.com/cron-id` label could exceed Kubernetes' 63-byte cap because the cron ID is `base36(appName === command === schedule)`, which expands roughly 1.5x per byte. The label is now keyed `dokku.com/cron-hash` and holds the `sha1` hex digest of the cron-id, a fixed 40-character value that always fits the cap. The same hex digest is mirrored into the `dokku.com/cron-hash` annotation, and the original base36 cron-id stays in the `dokku.com/cron-id` annotation that `cron:list` reads when surfacing user-facing IDs. Per-task lookups stay server-side via label selectors, so `cron:set --maintenance` and the forbid/replace concurrency checks on `dokku run --cron-id` keep working without any in-memory filtering.
Adds four Go template tests for ingress-route.yaml covering the imported-cert secret name, multiple domains per IngressRoute, the non-traefik ingress_class guard, and the https port_map dedup when an http port_map already covers the same container port. Also adds a bats integration test that deploys a TLS-enabled app under traefik and asserts both IngressRoute manifests and live http-to-https redirect plus https serving.
The canonical encode pipeline `echo "value" | base64` silently appends `\n` to the value, which round-trips through base64 and ends up stored in the env. Document that decoded values are stored byte-for-byte, recommend `printf '%s'` or `echo -n` for short inline values, and show `cat | base64 -w 0` for files. Adds a bats test asserting all four encoding patterns produce the expected stored bytes.
Closes#8647.
The asymmetric --logs-vector-global-image and --logs-vector-global-networks flags were renamed to --logs-global-vector-image and --logs-global-vector-networks in #8640 so they match the standard `--<plugin>-global-<property>` shape used by every other report. The bats description string at tests/unit/logs.bats:533 was still pinned to the old asymmetric phrasing, and there was no assertion that the deprecated JSON keys had actually been removed. Updates the test description to match its assertions and adds a regression guard that fails if logs-vector-global-image or logs-vector-global-networks ever reappear in the --global JSON report. Closes#8632.
The bare `cron` dependency caused `apt` to remove `dokku` whenever a user installed an alternative cron daemon such as `systemd-cron`. Listing the virtual `cron-daemon` package as an alternative keeps the default install behaviour (fresh installs still pull in `cron`) while letting any provider of `cron-daemon` satisfy the dependency, so swapping cron daemons no longer takes `dokku` with it.
The docker-options DOCKER_OPTIONS_<PHASE>.migrated sentinels and the storage data/storage-registry/migrations/<app> flag files are now stored as per-app properties (`migrated-<phase>` and `legacy-mounts-migrated` respectively), so they survive a property-store backup/restore and align with the existing `migrated-from-files`, `env-migrated`, and `nginx-conf-sigil-migrated` patterns. The new markers are written only when a non-empty legacy source was actually migrated, distinguishing apps that never had legacy state from apps that did and were drained. A one-cycle upgrade-cycle conversion drains any leftover filesystem sentinels into the new properties and removes the files; for docker-options the conversion deliberately runs before the global `migrated-from-files` short-circuit so installs upgrading from the previous release still get their `.migrated` files converted.
Renames the existing `## Internal properties` umbrella section to `## Properties` across all plugin docs and adds a new `### Settable properties` subsection enumerating every property accepted by each plugin's `:set` command. Each row lists scope, default, the actual `:report` flag shapes that surface the value, and a one-line description. The buildpacks plugin is included even though it uses `:set-property`, with a note in the subsection clarifying the legacy command name. Property lists, scopes, and report flags were verified against plugin source and the live dokku binary.
After the report rename made --logs-global-vector-image return the raw
stored value (empty when unset) and split --logs-computed-vector-image
out as the default-resolved key, the (logs:report)
vector-global-image and vector-global-networks raw test still pinned
assert_output_exists on the raw key in the unset state. The raw key is
correctly empty after dokku logs:set --global vector-image runs, and
the bundled vector image default is exposed via the computed key now.
The internal-properties tables on builds.md (8 --build-* flags),
ssl.md (8 --ssl-* flags), and process-management.md (4 ps flags) each
crammed all the flags into a single comma-separated table row with one
umbrella description. Splits each into a row per flag with a specific
description and the actual code-path source (the build record write
path, the openssl x509 parse target, the ps inspection helper).
The previous commit renamed --logs-vector-global-image and
--logs-vector-global-networks to --logs-global-vector-image and
--logs-global-vector-networks, and added --logs-computed-vector-image,
--logs-computed-vector-networks, and --logs-computed-vector-sink. The
logs.bats per-app Invalid-flag listing was still pinned to the old
10-flag set and the vector-global JSON keys and CLI flags still used
the old names, so the (logs) logs:report app, (logs:report)
vector-global-image and vector-global-networks raw, and (logs)
logs:set --global vector-networks tests failed - the latter cascaded
because the failing assertion ran before its vector-image cleanup.
Updates the per-app Invalid-flag string to the 13-flag sorted listing,
renames every stale jq key and CLI flag reference, adds a parallel
(logs) logs:report --global invalid flag test covering the 10-flag
global-scope listing, and adds dokku logs:set --global vector-image
to teardown so a future failing test doesn't leak the property.
Addresses review feedback on #8640. fn-archive-max-size,
fn-archive-max-files, fn-git-deploy-branch, and fn-git-keep-git-dir are
renamed with `computed` in the function name so the names match the
resolved-with-default semantic they implement, and every caller is
updated in lockstep. logs:report renames the misordered
--logs-vector-global-image and --logs-vector-global-networks to
--logs-global-vector-image and --logs-global-vector-networks, makes the
global getters return the raw stored value, and adds
--logs-computed-vector-image, --logs-computed-vector-networks, and
--logs-computed-vector-sink. network:report --global gains the
computed-* keys for attach-post-create, attach-post-deploy,
initial-network, and tld so external tooling can read the resolved
value at global scope. The --quiet flag is dropped from every
:report --format json | jq invocation across the PR's bats files; the
report's --format json output is the only stdout and the flag is
redundant.
The nginx-property binary's globalValue() switch returned ComputedX for
client-body-timeout, client-header-timeout, keepalive-timeout, and
lingering-timeout, leaking the per-app value into nginx:report's
global-<prop> key. Other properties in the same switch already call
GlobalX. Aligns the four with the rest. Also tightens
tests/unit/nginx-vhosts_properties.bats so the helper preserves values
containing whitespace or literal \$ across the inner /bin/bash -c, and
switches proxy-keepalive to integer values since the set subcommand
validates it via is_number.
Fixes CI failures from the prior commit's bats additions.
storage report mounts are prefixed with `-v `, logs.bats setup does not
auto-create the test app, the nginx-vhosts shell report emits JSON keys
without the `nginx-` plugin prefix, registry computed-image-repo falls
back to the default `dokku/<app>` repo name when no template is set,
cron computed-maintenance follows OR-not-override semantics so a per-app
`false` does not override a global `true`, the scheduler subcommand
help string is `Manage scheduler settings`, and bringing ps procfile-path
into the #8640 raw convention required updating the pre-existing
`(ps:set) procfile` assertion. Also extends fn-git-keep-git-dir to fall
back through the global property before the built-in default so global
keep-git-dir actually takes effect at deploy time.
The bats coverage for plugin :report keys has been uneven since the raw/global/computed split landed in PR #8640. This commit adds a single combined triplet test per settable property across app-json, builder, builder-dockerfile, builder-herokuish, builder-lambda, builder-nixpacks, builder-pack, builder-railpack, buildpacks, cron (maintenance), git (keep-git-dir, rev-env-var, source-image), logs (vector-sink, vector-global-image, vector-global-networks), network (attach-post-create, attach-post-deploy, initial-network), proxy (per-app type), ps (stop-timeout-seconds), registry (image-repo, push-on-release, push-extra-tags), storage (build/deploy/run mounts), and adds a new tests/unit/scheduler.bats and tests/unit/nginx-vhosts_properties.bats. Brings ps `procfile-path` and `stop-timeout-seconds` global getters in line with the #8640 convention (raw global, default resolved in computed). Documents the read-only and internal properties that surface in `:report` (or are written by Dokku) but cannot be managed by `:set` in a new `## Internal properties` section appended to each affected plugin's docs page.
The caddy and haproxy bats suites referenced `refute_output`, which is not provided by the in-tree test_helper, so the steps failed with `command not found`. The traefik `api-entry-point property` test and the logs `logs:set global` test still queried the removed bare `--traefik-api-entry-point` flag and the pre-split `--logs-global-max-size` default respectively. The assertions now use `assert_output_exists`, the `--traefik-computed-` flag, and the raw/computed key pair for `logs:set --global max-size`.
The `<plugin>-global-<property>` keys in `:report` output returned the resolved value with the built-in default substituted in, so external tooling could not distinguish a property that had been set globally to the default from one that had never been set. The bare keys for caddy, haproxy, and traefik global-only properties had the same shape. The `global-<property>` keys now hold the raw stored value and are empty when nothing has been set, and a new `computed-<property>` key holds the effective value used at runtime, falling back through the per-app value (where one exists), the global value, and the built-in default. The bare global-only keys for the three proxy plugins are removed in favor of the raw/computed pair. Closes#8631.
`cron:set --global` wrote the property but emitted `unknown flag: --global` because the post-set `scheduler-cron-write` trigger received `--global` as the appName arg, which pflag rejected before reaching the trigger body.
The trigger args now omit appName for global writes, and the `scheduler-k3s` cron-write trigger short-circuits when called without an app since per-app reconciliation requires a real app name. The docker-local trigger already regenerates the global crontab from all apps so global `mailfrom`/`mailto` are picked up without any further changes.
The Docker daemon refuses any endpoint settings on the default bridge network and `docker compose` unconditionally attaches the service name as an alias on every joined network, so combining bridge with user-defined networks via compose's `networks:` block is impossible. When `vector-networks` is set, the compose template now joins only the configured networks; outbound to external sinks still works through user-defined network NAT. Additionally, `vector-image` and `vector-networks` are both global-only but `common.CommandPropertySet` silently accepts them at app level by merging global-only keys into the valid-property set, so both now reject explicitly in `validateSetValue`.
The dokku entrypoint already exports DOKKU_ROOT, PLUGIN_PATH, PLUGIN_AVAILABLE_PATH, PLUGIN_ENABLED_PATH, PLUGIN_CORE_AVAILABLE_PATH and DOKKU_LIB_ROOT before invoking plugn. Delegating to `dokku plugin:trigger` keeps the helper at env-var parity with the production trigger path without enumerating every var the helper must forward.
Triggers that call verify_app_name or otherwise reference $DOKKU_ROOT fail under bats because the helper did not forward DOKKU_ROOT to the plugn subshell. The production dokku entrypoint exports DOKKU_ROOT before invoking plugn, so bring the helper to env-var parity.
Adds a new global `vector-networks` property on the logs plugin that takes a comma-separated list of Docker networks. When set, the rendered compose file declares each network plus `bridge` as external and joins them on the vector service, so `docker compose up` reconciles attachments on every `logs:vector-start`. When unset, the existing `network_mode: bridge` template is preserved unchanged. The value is validated against `docker network inspect` at set time, rejects the reserved `bridge` entry, and is surfaced in `dokku logs:report` via `--logs-vector-global-networks`.
The dokku entrypoint exports both vars before invoking plugn, so triggers that source `$PLUGIN_AVAILABLE_PATH/...` work in production. The helper only forwarded PLUGIN_PATH / PLUGIN_CORE_AVAILABLE_PATH / DOKKU_LIB_ROOT, so triggers that follow the same pattern fail under bats.
The new certs-set / certs-remove tests called plugn directly via /bin/bash -c, which exits with 'PLUGIN_PATH is not set in environment' under the bats runner. Route the calls through the existing run_plugn_trigger helper so the required PLUGIN_PATH / PLUGIN_CORE_AVAILABLE_PATH / DOKKU_LIB_ROOT vars are set.
The ports plugin's `post-certs-update` trigger was rewriting every `https:443:*` mapping from the app's `http:80:*` mappings, silently overwriting any user-defined mapping such as `https:443:443` used by apps that terminate TLS inside the container. The trigger now skips the rewrite when an `https:443:*` mapping already exists, keeping the default behavior only when the app has no explicit HTTPS mapping configured.
Closes#8619.
The bare `tls-internal` key previously returned the computed value, so external tooling could not tell whether the property had been set on the app or was merely defaulting to `false`. The property is now also configurable with `--global`, the report exposes `computed-tls-internal` and `global-tls-internal` keys alongside the bare raw key, and the deploy path honors the per-app value with a fallback to the global value before the built-in default. Closes#8625.
Adds `certs-set` and `certs-remove` plugin triggers so other plugins can install or remove an app's SSL cert/key pair without shelling out to the `dokku certs:add` / `dokku certs:remove` subcommands. Shared implementations live as `fn-certs-set` and `fn-certs-remove` in `plugins/certs/internal-functions`, with the subcommands and the new triggers calling `verify_app_name` before delegating.
# History
## 0.38.4
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.4/bootstrap.sh
sudo DOKKU_TAG=v0.38.4 bash bootstrap.sh
```
### Bug Fixes
- #8615: @josegonzalez Reject per-app sets for openresty global-only properties
- #8613: @josegonzalez Expose raw deploy-branch and keep-git-dir in git:report
- #8549: @josegonzalez Route CNB images through launcher on scheduler-k3s
### New Features
- #8614: @josegonzalez Split scheduler-docker-local report into raw, computed, and global
### Documentation
- #8603: @cheif Add `dokku-http-oauth` to community plugins
### Tests
- #8618: @josegonzalez Isolate scheduler-k3s registry tags per bats file
- #8616: @josegonzalez Migrate from junit_files to files in EnricoMi/publish-unit-test-result-action
- #8617: @josegonzalez Upgrade actions in shared build-image compose action
- #8609: @josegonzalez Skip packer lint job on dependabot PRs
- #8604: @dependabot[bot] chore(deps): bump python from 3.14.3-bookworm to 3.15.0b1-bookworm in /tests/apps/dockerfile-release
### Dependencies
- #8606: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.50.0 to 0.51.0 in /plugins/common
- #8608: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.45 to 2.11.46 in /plugins/scheduler-k3s
- #8607: @dependabot[bot] chore(deps): bump dokku/openresty-docker-proxy from 0.10.0 to 0.11.0 in /plugins/openresty-vhosts
- #8605: @dependabot[bot] chore(deps): bump python from 3.14.3-alpine to 3.15.0b1-alpine in /docs/_build
Parallel `unit.scheduler-k3s-*` matrix jobs all pushed to the same `savant/rdmtestapp:1` tag on Docker Hub, so a herokuish run pod could pull a CNB or dockerfile image that another job had just overwritten and fail with `exec: "/exec": stat /exec: no such file or directory`. The image-repo-template now embeds the bats file basename so each job owns its own tag namespace.
`openresty:set <app>` previously accepted per-app writes for properties whose readers only consult the global store, so `:set myapp image foo` printed a success message while `:report myapp` kept showing the global default. The per-app form is now rejected with `The key '<key>' can only be set globally`, matching the behavior introduced for `caddy`, `haproxy`, and `traefik` in #8602.
The bare `init-process` and `parallel-schedule-count` keys previously returned the computed value, so external tooling could not tell whether a property had been set on the app or was merely defaulting. Both properties are now also configurable with `--global`, the report exposes `computed-*` and `global-*` keys alongside the bare raw keys, and the deploy path honors the global value before falling back to the linuxserver.io vendor heuristic.
The bare `deploy-branch` and `keep-git-dir` keys in `git:report` returned the computed (effective) value rather than the raw per-app value, with no separate `computed-*` key to distinguish "set per-app" from "falling back to global or default". This left external tooling unable to detect a per-app unset without out-of-band state. The bare keys now hold the raw per-app value (empty when unset) and new `computed-deploy-branch` and `computed-keep-git-dir` keys hold the effective value, matching the convention used by `nginx-vhosts`, `network`, and `builder`. Closes#8610.
Dependabot PRs don't receive `secrets.DIGITALOCEAN_TOKEN`, so the `packer validate` step fails on every dependency bump. Guarding the job by PR author skips it cleanly while keeping it active for human PRs and pushes to `master`.
The cron-id label could exceed Kubernetes' 63-byte cap when commands or
schedules were long, and an all-digit job-suffix or cron-id rendered as
an unquoted YAML scalar caused the API server to reject manifests. Run
pods built from dockerfiles also occasionally hit the 10s startup wait
on a cold image pull, even though the pod was scheduled correctly.
The cron-id is now stored as an annotation and a shorter hash is used as
the selector label. Every interpolated annotation and label value in the
cron-job and deployment templates is now quoted to prevent numeric
coercion, and the run-pod wait timeout is raised to 30 seconds.
Extends bats coverage for the scheduler-k3s scheduler so that
herokuish and dockerfile builders match the cnb test surface for
`dokku run`, `dokku run:detached`, `dokku cron:run`, deployment
manifests, cronjob manifests, and Procfile-key resolution. Adds
the corresponding `app-cron-procfile.json` fixture for the python
app and `app-cron.json` / `app-cron-procfile.json` fixtures for
the dockerfile-procfile app.
After streaming logs from a run pod on scheduler-k3s, the apiserver may still report `PodRunning` for a short window due to kubelet status propagation lag, causing `dokku run` to fail with `Unable to attach as the pod is in an unknown state: Running`. Wait briefly for the pod to reach a terminal phase before classifying the outcome.
For short-lived commands the run pod can transition Running to Succeeded
between the running-pod check and the kubectl exec SPDY upgrade, leaving
the upgrade to fail with `container not found`. When stdout is not a TTY
(and DOKKU_FORCE_TTY is not set) the exec attach is only being used to
capture stdout, so stream the run pod logs in follow mode instead. The
pod's `TTLSecondsAfterFinished` of 60s keeps the kubelet's log file
readable for the duration of the call, eliminating the race.
Drop the assertion that the web deployment has no command since the python buildpack auto-emits a web Procfile entry, which correctly routes through launcher just like docker-local. Shorten the cron command fixture so the base36-encoded cron-id stays under the 63-byte kubernetes label limit, and relax the `dokku run` and `dokku cron:run` output assertions to `assert_output_contains` so they tolerate the leading blank line emitted on k3s.
Mirror the docker-local fix in #8525 for the k3s scheduler. CNB images default to a `/cnb/process/web` entrypoint that ignores incoming args, so non-web deployments, scheduled cron jobs, and ad-hoc `dokku run` / `cron:run` commands all need an explicit `launcher` entrypoint. The deployment and cron-job helm templates now set `command: [launcher]` when `image.type` is `pack`, and `TriggerSchedulerRun` sets the entrypoint to `launcher` for pack images while finishing the previously stubbed Procfile lookup branch so the resolved command is actually scheduled.
Pre-seeds the `dokku/install_default_site` debconf answer to `true` so the dokku postinst installs `/etc/nginx/conf.d/00-default-vhost.conf` during image build. Without this, debconf returned an empty value in non-interactive docker builds, the postinst's `setup-default-site` short-circuited, and nginx had no listener on port 80 - which left the readiness sentinel untouched and the container stuck unhealthy.
`caddy:set`, `haproxy:set`, and `traefik:set` previously accepted per-app writes for properties that only have a single host-wide reader, so `:set myapp image foo:bar` printed a success message while `:report myapp` kept showing the global default. The per-app form is now rejected with `The key '<key>' can only be set globally`, matching the existing rejection used for haproxy `refresh-conf` and traefik `challenge-mode`. Caddy `tls-internal` remains the only legitimate per-app property in this family.
The property name set via `app-json:set` is `appjson-path`, but the matching read-back flags on `app-json:report` were named `--app-json-selected`, `--app-json-global-selected`, and `--app-json-computed-selected`. The mismatch meant `dokku app-json:report <app> --app-json-appjson-path` (the form already documented in deployment-tasks.md) was rejected as an invalid flag, and the `--format json` output advertised keys that did not correspond to any settable property. The flags and JSON keys are renamed to `--app-json-appjson-path`, `--app-json-global-appjson-path`, and `--app-json-computed-appjson-path` so that the property name round-trips through set and report.
The `value_exists` variable in the report info-flag loop is no longer read after the `not deployed` failure was removed, and was already unused in domains, haproxy-vhosts, traefik-vhosts, caddy-vhosts, and openresty-vhosts. Drop the declaration and the trailing assignment so the loop reads cleanly across all plugins.
Several plugin `:report` subcommands erroneously failed with `not deployed` when an info-flag matched a property that was empty. Empty values are legitimate for configuration properties pre-deploy and the `--format json` path already returns them without error. Remove the `value_exists` check across nginx, checks, git, certs, scheduler-docker-local, and the builder-* plugins so the info-flag form behaves consistently with the JSON form.
Adds a bats lint test that guards the static wiring (nginx conf, dokku-restore finish-script ordering, my_init sentinel reset, and the Dockerfile HEALTHCHECK line) plus a docker smoke test that boots the built image, waits for the health flip, exercises the loopback endpoint, asserts the port is not published to the host, and verifies the negative path. The smoke test is invoked via a new `make test-image-healthcheck` target and runs automatically in the build-image action after `docker buildx --load`.
The official dokku/dokku image gains a HEALTHCHECK directive backed by a loopback-only HTTP endpoint at `127.0.0.1:18080/_dokku/health`. The endpoint reports 200 once first-boot bootstrap finishes, sshd and nginx are accepting connections, and `dokku ps:restore` completes; otherwise it returns 503. Changes are scoped to the Docker overlay and Dockerfile so debian-package installs are unaffected.
The shipped catch-all default site uses `ssl_reject_handshake`, which is unsupported on nginx older than 1.19.4 and causes nginx to fail to start on Debian Bullseye. The postinst now detects the installed nginx version and installs an HTTP-only variant of the catch-all on older systems.
The security fix that quoted `$APP` inside the pre-receive hook heredoc changed the literal hook contents from `dokku git-hook foo` to `dokku git-hook "foo"`, so the existing substring assertions no longer match.
The bash and go validators previously each kept their own copy of the regex, which had to be updated in lockstep. Both bash wrappers now invoke the existing common binary via the same pattern as `verify_app_name`, leaving go as the single source of truth. The legacy `IsValidAppNameOld` rule is also widened to allow underscores again so apps created under the old naming rules can still be looked up through `VerifyAppName`'s either-rule fallback.
The previous app name validation regex permitted shell metacharacters such as `;`, `$`, backticks, `|`, and `&`. These names were embedded unquoted into the generated git pre-receive hook script, allowing an authenticated user to execute arbitrary commands as the dokku user simply by pushing to a remote with a crafted app name. App names are now restricted to lowercase alphanumerics, dots, and hyphens, and the hook script also quotes the app variable as a defense-in-depth measure.
Bats runs tests under `set -eo pipefail`, so when `grep -F -o` finds nothing inside the count pipe it exits 1, the whole pipe fails, errexit fires, and the function aborts before reaching the count comparison. Wrap grep in `{ ... || true; }` so the pipe stays zero when the pattern is absent and the helper falls through to the flunk message.
Replaces the `DOKKU_ARCHIVE_MAX_SIZE` and `DOKKU_ARCHIVE_MAX_FILES` environment variables with global git properties (`archive-max-size` and `archive-max-files`), configurable via `dokku git:set --global` and surfaced through `dokku git:report --global`. Defaults remain `1073741824` bytes and `10000` entries.
Archives passed to git:from-archive and certs:add were extracted without symlink or path validation, allowing a crafted archive to write arbitrary files anywhere writable by the dokku user via symlink traversal. Extraction now pre-scans entries for absolute paths, parent traversal, and unsafe symlinks, applies the GNU tar `--no-unsafe-links` flag when available, and validates symlinks after extraction.
The previous use of `touch` before `netrc set` allowed the file to inherit the umask and be world-readable, exposing stored git credentials to local users. The set and unset paths now explicitly chmod 0600 and chown to the dokku user, and the plugin install hook repairs permissions on already-affected installations.
Add defense-in-depth sanitization for OpenResty include files to prevent
OS command injection via malicious filenames that break shell quoting in eval.
- Add filename validation in core-post-extract using regex [^a-zA-Z0-9_.-]
- Validate both http-includes and location-includes paths
- Abort deploy via dokku_log_fail on unsafe filenames
- Skip non-regular files (symlinks, directories) during extraction
- Add security regression test with unsafe filename containing space
- Keep existing guards in docker-args-process-deploy as belt-and-suspenders
- Update documentation to clarify allowed filename characters
Addresses CVSS 9.9 vulnerability where filenames like poc'$(cmd)'x.conf
could escape shell quoting and execute arbitrary commands during deploy.
Replace the bash pattern-substitution loop with grep -F -o piped to wc -l so the helper counts literal substring occurrences instead of treating the expected value as a glob pattern. The old implementation interpreted `[`, `]`, `*`, `?`, and `\` as pattern syntax, which made `assert_output_contains "['task.py', 'test']"` report 17 matches against an output that contained the string exactly once - the inner characters were being matched as a character class. assert_output_not_contains delegates to assert_output_contains and is fixed transitively.
The previous form set `trap "rm -rf '$TMP_DIR'" RETURN` inside the test, but bats propagates `RETURN` traps to nested function calls, so the trap fired on the first `assert_success` and removed the work directory before the trigger script ran. Switching to bats's per-test `BATS_TEST_TMPDIR` removes the trap entirely and lets bats handle cleanup.
Both helpers wrap `run /bin/bash -c "..."` with the env vars dokku plugin scripts and `plugn` need, replacing the long inline boilerplate that was duplicated across `tests/unit/resource_3.bats` and the new `core-post-extract` regression tests in the builder bats files.
The builder-dockerfile, builder-lambda, builder-nixpacks, builder-pack and builder-railpack `core-post-extract` triggers assigned `$2` to a local `SOURCECODE_WORK_DIR` but called `pushd "$TMP_WORK_DIR"`, which was unset. Bash 5.2 silently accepted `pushd ""`, so the bug stayed dormant. Bash 5.3 (shipped with Ubuntu 26.04) makes it a hard error and `set -e` aborts the trigger, causing every `git push` to fail with `pushd: null directory`.
* master: (35 commits)
Release 0.37.9
chore(deps): bump github.com/go-acme/lego/v4 in /plugins/scheduler-k3s
chore(deps): bump github.com/moby/spdystream in /plugins/scheduler-k3s
chore(deps): bump github.com/go-openapi/jsonpointer
chore(deps): bump k8s.io/api in /plugins/scheduler-k3s
Release 0.37.8
chore(deps): bump k8s.io/apimachinery in /plugins/scheduler-k3s
chore(deps): bump k8s.io/kubernetes in /plugins/scheduler-k3s
chore(deps): bump packaging from 26.0 to 26.1 in /docs/_build
chore(deps): bump github.com/fluxcd/pkg/kustomize
chore(deps): bump zipp from 3.23.0 to 3.23.1 in /docs/_build
fix(deps): pin controller-runtime to v0.22.4 for keda compatibility
chore(deps): bump github.com/cert-manager/cert-manager
chore: bump dependencies in tests/apps/php
chore: upgrade traefik from v2.11.41 to v2.11.42
chore: bump go modules
chore(deps): bump mvdan.cc/sh/v3 from 3.13.0 to 3.13.1 in /plugins/cron
chore: bump dependencies in tests/apps/multi
chore: bump go modules
chore: bump go modules
...
* master: (68 commits)
chore(deps): bump werkzeug in /tests/apps/python-flask
chore(deps): bump github.com/go-jose/go-jose/v4
chore(deps): bump rack from 3.2.5 to 3.2.6 in /tests/apps/ruby
chore(deps): bump pymdown-extensions in /docs/_build
Add application/graphql-response+json to nginx gzip_types
chore(deps): bump google.golang.org/grpc in /tests/apps/gogrpc
chore(deps): bump pygments from 2.19.2 to 2.20.0 in /docs/_build
chore(deps): bump golang.org/x/crypto in /plugins/common
chore: bump go modules
chore(deps): bump github.com/fatih/color in /plugins/common
chore(deps): bump brace-expansion in /tests/apps/multi
chore(deps): bump gunicorn in /tests/apps/python-flask
chore(deps): bump path-to-regexp in /tests/apps/checks-root
chore(deps): bump gunicorn in /tests/apps/dockerfile-release
chore(deps): bump traefik in /plugins/traefik-vhosts
chore(deps): bump gunicorn from 25.2.0 to 25.3.0 in /tests/apps/multi
chore(deps): bump werkzeug in /tests/apps/python-flask
chore(deps): bump picomatch from 2.3.1 to 2.3.2 in /tests/apps/multi
chore(deps): bump gunicorn in /tests/apps/dockerfile-release
chore(deps): bump djangorestframework in /tests/apps/dockerfile-release
...
* master: (427 commits)
chore(deps): bump gunicorn from 25.0.2 to 25.1.0 in /tests/apps/multi
chore(deps): bump flask from 3.1.2 to 3.1.3 in /tests/apps/multi
chore(deps): bump python in /docs/_build
chore(deps): bump google.golang.org/grpc in /tests/apps/gogrpc
chore(deps): bump qs from 6.14.1 to 6.14.2 in /tests/apps/checks-root
chore(deps): bump rack from 3.2.4 to 3.2.5 in /tests/apps/ruby
fix: call correct function for limiting letsencrypt to certain domains
chore(deps): bump gunicorn in /tests/apps/python-flask
chore(deps): bump whitenoise in /tests/apps/dockerfile-release
chore(deps): bump phusion/baseimage from noble-1.0.2 to noble-1.0.3
chore(deps): bump actions/upload-artifact from 6 to 7
chore(deps): bump actions/download-artifact from 7 to 8
chore(deps): bump github.com/traefik/traefik/v2
chore(deps): bump gunicorn in /tests/apps/dockerfile-release
chore(deps): bump flask from 3.1.2 to 3.1.3 in /tests/apps/python-flask
chore(deps): bump traefik from 3.6.7 to 3.6.9 in /plugins/traefik-vhosts
chore(deps): bump mkdocs-material from 9.7.1 to 9.7.3 in /docs/_build
chore(deps): bump byjg/easy-haproxy in /plugins/haproxy-vhosts
chore(deps): bump dj-database-url in /tests/apps/dockerfile-release
chore(deps): bump werkzeug in /tests/apps/python-flask
...
# Conflicts:
# common.mk
# contrib/dependencies.json
You can then proceed to configure your server domain (via `dokku domains:set-global`) and user access (via `dokku ssh-keys:add`) to complete the installation.
HISTORY="${HISTORY}"$'\n\n'"See the [${NEXT_VERSION} migration guide](/docs/appendices/${NEXT_VERSION}-migration-guide.md) for more information on migrating to ${NEXT_VERSION}."
@@ -96,6 +96,14 @@ System administrators are highly encouraged to store persistent data in app-spec
See the [persistent storage documentation](/docs/advanced-usage/persistent-storage.md) for more information on how to attach persistent storage to your app.
### Restoring on a machine with a different CPU Architecture
When restoring a backup on a machine that has a different CPU architecture, you will need to clear out the `~/.basher` as it's not portable across different CPU architectures. Do so with the following command immediately after restoring your dokku configs:
```shell
rm -rf /home/dokku/.basher
```
## Recovering app code
In case of an emergency when your git repo and backups are completely lost, you can recover the last pushed copy from your remote Dokku server (assuming you still have the ssh key).
-`--builds-computed-retention`: the resolved retention applied to this app
`--build-status` returns the **display** status, so an abandoned in-flight build shows `abandoned` rather than `running`. The raw on-disk status is only visible by reading the JSON record directly.
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `builds:report`. The JSON keys emitted by `builds:report --format json` are the same names with the leading `--builds-` stripped (e.g. `retention`, `global-retention`, `computed-retention`). Legacy keys with the `builds-` prefix (e.g. `builds-retention`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release. Status keys (`build-id`, `build-status`, etc.) have no plugin prefix and are unaffected.
| `retention` | app + global | `20` | `--builds-retention`, `--builds-global-retention`, `--builds-computed-retention` | Number of recent build records kept per app; older finalized records are pruned at the end of each deploy |
### Read-only flags
The following flags surface in `builds:report` but are not managed by `builds:set` - they are derived metadata recorded during the build:
| Flag | Description |
|---|---|
| `--build-id` | Unique identifier of the most recent build for the app |
| `--build-kind` | Whether the record was a `build` or a `deploy` |
| `--build-status` | Display status (`running`, `succeeded`, `failed`, `canceled`, `abandoned`); computed at read time |
| `--build-source` | Trigger that started the build (e.g. `git-hook`, `ps:rebuild`, `ps:restart`) |
| `--build-pid` | PID of the build process |
| `--build-started-at` | UNIX timestamp the build started |
| `--build-finished-at` | UNIX timestamp the build finished |
| `--build-exit-code` | Exit code of the build process |
The `appjson-path` and `global-appjson-path` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-appjson-path` key holds the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default of `app.json`.
@@ -38,6 +38,16 @@ More information on supported Docker options can be found [here](https://docs.do
Container options configured via the `docker-options` plugin are not used to modify the process a container runs. Container options are the `[OPTIONS]` portion of the following, where `[CONTAINER_COMMAND]` and `[ARG]` are the process and the arguments passed to it that are launched in the created container: `docker run [OPTIONS] [CONTAINER_COMMAND] [ARG...]`. Please see the documentation for [customizing the run command](/docs/deployment/builders/dockerfiles.md#customizing-the-run-command) or use a [Procfile](/docs/deployment/builders/dockerfiles.md#procfiles-and-multiple-processes) to modify the command used by a Dockerfile-based container.
#### Scheduler support
Docker options are written in Docker's own vocabulary and are passed verbatim to `docker run` by the `docker-local` scheduler. Other schedulers translate only the subset that has an equivalent in their own runtime, and ignore the rest.
The `k3s` scheduler translates `--cap-add`, `--cap-drop`, `--privileged`, and `--sysctl` into their Kubernetes equivalents. See the [k3s scheduler documentation](/docs/deployment/schedulers/k3s.md) for details, including the restriction that only namespaced sysctls can be set on a pod.
Docker supports volume and host directory mounting via the `-v` or `--volume` flags. In order to simplify usage, Dokku provides a `storage` plugin as an abstraction to interact with persistent storage. In most cases, the Dokku project recommends using the persistent storage plugin over directly manipulating docker options at different phases. See the [persistent storage documentation](/docs/advanced-usage/persistent-storage.md) for more information on how to attach persistent storage to your app.
@@ -64,7 +74,21 @@ Multiple docker options can also be specified in a single call. Each `--flag [va
A misplaced `--process PROC` (i.e. one specified after the app name instead of before it) is honored as a subcommand flag rather than stored as a docker option, so the example above and the equivalent process-scoped form below behave identically:
Option values are stored and passed to the container verbatim. Quoting only controls how a value is split into words - no shell expansion is performed, so `$(...)`, backticks, `$VAR`, and globs are treated literally rather than being interpreted by the shell. This is what lets values such as a Traefik router rule be applied as-is:
> Options added before 0.38.25 were expanded by the shell when a container was created. Any such option that relied on shell expansion - `$(...)`, backticks, or `$VAR` - is treated as a literal string after upgrading and must be re-added with the value already resolved:
A misplaced `--process PROC` (i.e. one specified after the app name instead of before it) is honored as a subcommand flag rather than stored as a docker option, so the two invocations below behave identically:
```shell
dokku docker-options:add --process web node-js-app deploy "--ulimit nofile=12""--shm-size 256m"
@@ -91,6 +115,13 @@ Multiple docker options can also be removed in a single call, mirroring the spli
A stored option is matched by shell word rather than by exact string, so the value only has to be equivalent to the stored one, not byte-identical. Quoting an option one way removes an option that was stored quoted another way:
The JSON report includes the existing string keys (`build`, `deploy`, `run`, and `deploy.<process>` when configured) plus parallel `-list` keys for the same shorthand keys. Each `-list` value is a JSON array with one element per option as originally stored via `docker-options:add`, which allows export tooling to round-trip options that contain spaces without splitting the legacy space-joined strings. Empty phases emit an empty array (`[]`). The deprecated `docker-options-*` prefixed keys are unchanged and do not gain `-list` companions.
The following properties are recorded internally by the docker-options plugin and are not exposed via `docker-options:report`:
| Property | Scope | Description | Source |
|---|---|---|---|
| `migrated-from-files` | global | Global migration sentinel that records that the legacy `DOCKER_OPTIONS_<PHASE>` flat-file store has been drained into plugin properties | `plugins/docker-options/functions.go` writes `"true"` once the install-time migration runs |
| `migrated-build` | per-app | Per-app marker recording that the app's legacy `DOCKER_OPTIONS_BUILD` file was drained into the `_default_.build` property list. Only set when the legacy file contained non-empty content | `plugins/docker-options/functions.go` writes `"true"` after the per-phase drain |
| `migrated-deploy` | per-app | Per-app marker recording that the app's legacy `DOCKER_OPTIONS_DEPLOY` file was drained into the `_default_.deploy` property list. Only set when the legacy file contained non-empty content | `plugins/docker-options/functions.go` writes `"true"` after the per-phase drain |
| `migrated-run` | per-app | Per-app marker recording that the app's legacy `DOCKER_OPTIONS_RUN` file was drained into the `_default_.run` property list. Only set when the legacy file contained non-empty content | `plugins/docker-options/functions.go` writes `"true"` after the per-phase drain |
| `migrated-traefik-backticks` | global | Global sentinel recording that stored Traefik labels whose backticks carried a stray backslash have been repaired | `plugins/docker-options/functions.go` writes `"true"` once the install-time repair runs |
| `migrated-canonical-options` | global | Global sentinel recording that stored options have been rewritten into the canonical form, quoting values whose shell metacharacters were left bare by an older legacy-file drain and splitting entries that carried several flags | `plugins/docker-options/functions.go` writes `"true"` once the install-time rewrite runs |
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:
@@ -92,12 +110,130 @@ By default, permissions are set for usage with Herokuish buildpacks. These permi
-`--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
> [!IMPORTANT]
> New as of 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`:
```shell
dokku storage:create node-js-data --mode 0777
```
```shell
dokku storage:set node-js-data mode 0770
```
Omitting the value clears the mode, leaving the directory's permissions alone on subsequent runs:
```shell
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`:
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
> [!IMPORTANT]
> The property form is new as of 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:
```shell
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 |
| `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:
! 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
> [!IMPORTANT]
> New as of 0.38.27
Annotations and labels are attached to a storage entry one key at a time, matching the [scheduler-k3s equivalents](/docs/deployment/schedulers/k3s.md#setting-annotations). On k3s they propagate to both the PersistentVolumeClaim and the PersistentVolume, so backup tools like Velero and Longhorn can find the volume.
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:
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:
`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.
@@ -116,6 +252,17 @@ In the first example, Dokku will then mount the shared contents of `/var/lib/dok
> 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`):
```shell
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](/docs/processes/process-management.md) for more information.
```shell
@@ -140,6 +287,65 @@ Once persistent storage is unmounted, the app requires a restart. See the [proce
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.
```shell
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"
> rdmtest-entry
-----> Storage entry rdmtest-entry destroyed
```
The confirmation may be avoided by providing the `--force` flag, which is useful for non-interactive or automated callers:
```shell
dokku storage:destroy rdmtest-entry --force
```
The global `--force` flag is also supported:
```shell
dokku --force storage:destroy rdmtest-entry
```
#### Removing the host directory
> [!IMPORTANT]
> New as of 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:
! 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:
`--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
> [!IMPORTANT]
@@ -185,6 +391,60 @@ You can pass flags which will output only the value of the specific information
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`):
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:
@@ -227,3 +487,13 @@ By default, Dokku will execute your buildpack app processes as the `herokuishuse
> 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/deployment/schedulers/docker-local.md#disabling-chown-of-persistent-storage) 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 |
plugin:install [--core|git-url] [--committish branch|commit|tag] [--name custom-plugin-name] [--skip-install-trigger] # Optionally download git-url (and pin to the specified branch/commit/tag) & run install trigger for active plugins (or only core ones)
plugin:installed <name> # Checks if a plugin is installed
plugin:install-dependencies [--core] # Run install-dependencies trigger for active plugins (or only core ones)
plugin:list # Print active plugins
plugin:list [--format stdout|json] # Print active plugins
plugin:trigger <args...>. # Trigger an arbitrary plugin hook
plugin:uninstall <name> # Uninstall a plugin (third-party only)
plugin:update [name [branch|commit|tag]] # Optionally update named plugin from git (and pin to the specified branch/commit/tag) & run update trigger for active plugins
@@ -36,37 +36,70 @@ dokku plugin:list
```
plugn: dev
00_dokku-standard 0.38.1 enabled dokku core standard plugin
The list can also be emitted as JSON via the `--format json` flag. In addition to the name, version, enabled state, and description shown in the default output, the JSON output includes whether a plugin is a core plugin and - for git-based third-party plugins - the install source (the git remote URL, the currently checked-out commit, and the followed branch). This is useful for tooling that reconstructs a server's set of installed plugins:
The `source_url`, `committish`, and `branch` fields are only populated for plugins installed from a git repository. They are empty for core plugins as well as for plugins installed from a tarball or a local `file://` path. When a plugin is pinned to a specific commit or tag (a detached checkout), the `branch` field is empty while `committish` still reports the exact commit.
> [!WARNING]
> All plugin commands other than `plugin:list` and `plugin:help` require sudo access and must be run directly from the Dokku server.
Instead of setting the image repository name on a per-app basis, it can be set via a template globally with the `image-repo-template` property:
Instead of setting the image repository name on a per-app basis, it can be set via a template with the `image-repo-template` property. The property can be set globally or for a specific app, with the per-app value overriding the global value when both are present:
Dokku uses a Golang template and has access to the `AppName` variable as shown above.
Dokku uses a Golang template and has access to the `AppName` variable as shown above. The per-app `image-repo` property always takes precedence over the rendered template when both are set.
Setting the property value to an empty string will reset the value to the system default. Resetting the value can be done per app or globally.
When `push-on-release` is enabled, Dokku treats the remote registry as the canonical store for app images. If a local image disappears - for example because a `docker image prune` cron ran, the host rebooted, or an operator removed it manually - subsequent commands that need the image (`ps:restart`, `ps:scale`, `dokku run`, `domains:add`, `certs:add`, etc.) will pull the missing tag back from the registry automatically. Per-app registry credentials configured via `registry:login` are honored during the pull. This recovery covers the deployed numeric tag pushed by the registry plugin - a missing `latest` tag will be ignored.
### Push extra tags
To push the image on release with extra tags, set the `push-extra-tags` to a comma-separated list of tags via the `registry:set` command. The default value for this property is empty. Setting the property will result in the image being tagged with extra tags every release.
> The `Report flags` column lists the CLI argument names accepted by `registry:report`. The JSON keys emitted by `registry:report --format json` are the same names with the leading `--registry-` stripped (e.g. `image-repo`, `global-server`, `computed-push-on-release`). Legacy keys with the `registry-` prefix (e.g. `registry-image-repo`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `image-repo` | app only | `dokku/<app>` | `--registry-image-repo`, `--registry-computed-image-repo` | Repository name used when pushing the app's image (overrides the global template) |
| `image-repo-template` | app + global | none | `--registry-image-repo-template`, `--registry-global-image-repo-template`, `--registry-computed-image-repo-template` | Go template used to compute the per-app image repository when `image-repo` is unset |
| `push-extra-tags` | app + global | none | `--registry-push-extra-tags`, `--registry-global-push-extra-tags`, `--registry-computed-push-extra-tags` | Comma-separated list of additional tags pushed alongside the deploy tag |
| `push-on-release` | app + global | `false` | `--registry-push-on-release`, `--registry-global-push-on-release`, `--registry-computed-push-on-release` | When `true`, pushes the image to the registry on every successful build |
| `server` | app + global | none | `--registry-server`, `--registry-global-server`, `--registry-computed-server` | Registry server host (e.g. `ghcr.io`) used when pushing images |
- The path on disk to both the global `ENV` file and app `ENV` files have been moved. Users should reference environment variables via the provided plugin triggers rather than directly sourcing the ENV files. Existing ENV files are left untouched and will be removed on the subsequent Dokku install.
- The path on disk to both the global `ENV` file and app `ENV` files have been moved. Users should reference environment variables via the provided plugin triggers rather than directly sourcing the ENV files. Existing ENV files are merged into the new location and removed once they have been drained. **Changed in 0.38.26:** removal previously happened on the subsequent Dokku install for app ENV files, and never happened at all for the global ENV file. **Changed in 0.38.27:** an ENV file found at the old path after its migration has been recorded is never merged into the new location, because the new location holds every change made since. Such a file is removed when its values agree with the current config, and otherwise moved aside to `ENV.migrated` with the keys it disagrees on named in a warning, so its values can still be applied by hand with `dokku config:set`. That copy is left for review and should be deleted afterwards: Dokku no longer reads it, and it holds everything it was not allowed to import - including keys that were unset on purpose, which for a revoked secret means a copy lingering on disk.
- During a fresh apt install, the upstream nginx default vhost files (`/etc/nginx/sites-enabled/default`, `/etc/nginx/sites-available/default`, and `/etc/nginx/conf.d/default.conf`) are renamed to `${path}.dokku-disabled` (not deleted) to avoid a `duplicate default server for 0.0.0.0:80` error. Operators with local customizations can recover them by inspecting the `.dokku-disabled` siblings. Upgrade-in-place installs do not touch any existing nginx files.
- Fresh apt installs now ship a catch-all default site at `/etc/nginx/conf.d/00-default-vhost.conf` that rejects requests with unknown Host headers using `ssl_reject_handshake on` (HTTPS) and `return 444` (HTTP). This replaces the manual workaround previously documented in the nginx docs. The behavior can be opted out at install time via the `dokku/install_default_site` debconf prompt. See the [Default site documentation](/docs/networking/proxies/nginx.md#default-site).
- Fresh apt installs now ship a catch-all default site at `/etc/nginx/conf.d/00-default-vhost.conf` that rejects requests with unknown Host headers using `ssl_reject_handshake on` (HTTPS) and `return 444` (HTTP). This replaces the manual workaround previously documented in the nginx docs. The behavior can be opted out at install time via the `dokku/install_default_site` debconf prompt. On nginx older than 1.19.4 (e.g., Debian Bullseye's nginx 1.18.0), the postinst installs an HTTP-only variant of the catch-all that omits the SSL listener and `ssl_reject_handshake`, since that directive is unsupported on those versions. See the [Default site documentation](/docs/networking/proxies/nginx.md#default-site).
- The `docker-local` scheduler now sends `SIGTERM` to old containers immediately after a successful deploy, rather than waiting `wait-to-retire` seconds before signaling. This matches Heroku's graceful-shutdown contract and lets applications begin draining in-flight work as soon as proxy traffic switches. The `wait-to-retire` grace period and `stop-timeout-seconds` hard-stop continue to apply as before. See the [zero downtime deploys documentation](/docs/deployment/zero-downtime-deploys.md#wait-to-retire) for more details.
- The `docker-local` scheduler no longer queues an image for retirement when another running container of the same app still uses it. This fixes the case where a `ps:rebuild` against an image-based deploy (`git:from-image`) produced an identical-SHA image and the `dokku-retire` cron timer would log `Image ... has running containers, skipping rm` on every run. Stuck entries from prior versions are pruned automatically on the next `ps:retire` run.
- All `:report` subcommands now accept the `--global` flag, which scopes the report to globally-configured properties. The flag composes with `--format json`, so a JSON report of global properties can be obtained via, for example, `dokku scheduler:report --global --format json`. Previously, combining `--global` with `--format json` was rejected with an "info flag" error, and `--global` on its own was treated as an unknown flag.
- Every `:report` key of the form `<plugin>-global-<property>` now returns the raw stored global value (empty when the property has never been set) instead of the resolved value with the built-in default substituted in. A new `<plugin>-computed-<property>` key has been added wherever a default existed and returns the effective value (per-app value, falling back to the global value, then to the built-in default). This affects `app-json`, `builder-dockerfile`, `builder-herokuish`, `builder-lambda`, `builder-nixpacks`, `builder-pack`, `builder-railpack`, `caddy`, `checks`, `git`, `haproxy`, `logs`, `network`, `nginx`, `openresty`, `proxy`, `registry`, `scheduler`, `scheduler-k3s` and `traefik`. External tooling that read `<plugin>-global-<property>` and depended on the default value should switch to `<plugin>-computed-<property>`. The bare `<plugin>-<property>` keys for caddy/haproxy/traefik/openresty global-only properties (`caddy-image`, `haproxy-log-level`, `traefik-api-enabled`, `openresty-image`, `openresty-letsencrypt-email`, `openresty-letsencrypt-server`, `openresty-allowed-letsencrypt-domains-func-base64`, etc.) have also been replaced by the `global-` and `computed-` pair.
- The `ps` and `cron` plugins now follow the same `<plugin>-global-<property>` / `<plugin>-computed-<property>` convention. The `ps:report` keys `stop-timeout-seconds`, `global-stop-timeout-seconds`, and `computed-stop-timeout-seconds` have been renamed to `ps-stop-timeout-seconds`, `ps-global-stop-timeout-seconds`, and `ps-computed-stop-timeout-seconds`. The `cron:report` keys `cron-mailfrom` and `cron-mailto` have been renamed to `cron-global-mailfrom` and `cron-global-mailto`, and new `cron-computed-mailfrom` and `cron-computed-mailto` keys have been added. The corresponding user-facing `--<flag>` arguments to `ps:report` and `cron:report` were renamed alongside the JSON keys; no aliases are kept. Additionally, both `ps:report --global` and `cron:report --global` now emit the `<plugin>-computed-<property>` keys for every settable global property with a default (`ps-computed-procfile-path`, `ps-computed-stop-timeout-seconds`, `cron-computed-maintenance`, `cron-computed-mailfrom`, `cron-computed-mailto`), matching the shape used by the other plugins.
- A second round of `:report` additions surfaces every remaining settable-but-unreported property under the same raw/global/computed convention so external tooling can verify drift via `:report --format json` without falling back to a generic bash task. The `ps` plugin gains `--ps-dockerfile-start-cmd` and `--ps-computed-dockerfile-start-cmd`, `--ps-start-cmd` and `--ps-computed-start-cmd`, and the `--ps-skip-deploy` / `--ps-global-skip-deploy` / `--ps-computed-skip-deploy` triple (default `false`). The `builder` plugin gains the `--builder-skip-cleanup` triple (default `false`). The `scheduler` plugin gains the `--scheduler-shell` triple. The `proxy` plugin gains the `--proxy-proxy-port` and `--proxy-proxy-ssl-port` triples and exposes the raw `disabled` property as `--proxy-disabled` / `--proxy-computed-disabled`, alongside the existing inverted `--proxy-enabled`. The `openresty` plugin gains `--openresty-global-log-level` and `--openresty-computed-log-level` (default `ERROR`). The `nginx` plugin gains the `--nginx-nginx-service-command` triple. The `scheduler-k3s` plugin gains `--scheduler-k3s-global-token`, but the value is masked as `*******` in default stdout output; the raw value is returned only when the report is requested via `--format json` or when this flag is queried explicitly by name. The traefik `dns-provider-<env_var>` keys are reported as `--traefik-global-dns-provider-<env_var>` and follow the same masking and explicit-query rules, as does the traefik `basic-auth-password` property, whose `--traefik-global-basic-auth-password` and `--traefik-computed-basic-auth-password` keys are masked in default stdout output.
- All Go-implemented plugins (`app-json`, `apps`, `builder`, `buildpacks`, `builds`, `cron`, `docker-options`, `logs`, `network`, `ports`, `proxy`, `ps`, `registry`, `resource`, `scheduler`, `scheduler-k3s`, `storage`) now emit JSON keys from `:report --format json` without the `<plugin>-` head segment, matching the shape bash plugins have always emitted. For example, `dokku ps:report myapp --format json` now contains `stop-timeout-seconds`, `global-stop-timeout-seconds`, and `computed-stop-timeout-seconds` keys. The CLI flag names (`--ps-stop-timeout-seconds`, etc.) are unchanged, and `:set` semantics are unchanged. For backwards compatibility during the 0.38.x patch series, the old `<plugin>-<property>` JSON keys are emitted side-by-side with the new keys, so external scripts reading either shape continue to work. The legacy keys will be dropped in a future major release. External JSON consumers should migrate to the new key shape.
- The `scheduler-k3s` plugin now manages env config and the dokku-generated image pull Secret as their own helm releases with stable names (`config-{app}` and `pull-secret-{app}`) rather than bundling them into the app helm chart with a per-deploy timestamp suffix (`env-{app}.{ts}` / `ims-{app}.{ts}`). This fixes two bugs: a helm rollback of the app chart no longer deletes Secrets that older ReplicaSets still reference, and the Deployment's `imagePullSecrets` list no longer accumulates references to nonexistent Secrets across deploys. The next deploy of an app switches the Deployment's `envFrom` and `imagePullSecrets` references to the stable names and prunes any leaked entries; existing live Deployments do not need to be patched manually. App rename now also uninstalls the old `tls-{app}`, `config-{app}`, and `pull-secret-{app}` releases under the previous app name; the new name's releases are recreated on the next deploy or certs sync.
- **New in 0.38.25:** Values supplied through docker options, `dokku run`'s `-e`/`--env` flag, and `--ttl-seconds` are no longer evaluated by the shell when assembling a container's arguments; they are now tokenized and passed through verbatim. This closes a command-injection vector where a `$(...)` or backtick expression in one of these values executed on the host as the `dokku` user during build, deploy, or run. As a result, shell metacharacters such as `$(...)`, backticks, `$VAR`, and globs in these values are treated literally instead of being expanded, and `--ttl-seconds` must now be a plain integer. Existing Traefik docker-options labels (those whose label key begins with `traefik.`) whose backticks were stored with a stray backslash are repaired automatically the first time `dokku` runs after the upgrade, so they become valid on the next deploy.
- **New in 0.38.26:** Docker options drained out of the pre-0.38.0 `DOCKER_OPTIONS_<PHASE>` files were copied verbatim rather than being re-serialized the way `docker-options:add` stores them, so `docker-options:remove` compared the canonical string it builds against a value that could never match it and exited successfully without removing anything. Removal now matches stored options by shell word instead of by exact string, so those entries can be removed with the value as originally written. Stored options are also rewritten into the canonical form the first time `dokku` runs after the upgrade: values whose shell metacharacters were left unquoted are quoted, and an entry that carried several flags on one line becomes one entry per flag, which additionally fixes `ps:report` reading a restart policy off such a line and the `k3s` scheduler translating only the first of several `--cap-add`/`--sysctl` flags. Options added through `docker-options:add` are already canonical and are left untouched.
- **New in 0.38.26:** Deprecated `DOKKU_*` config variables belonging to plugins whose install step runs before the `config` plugin's were not migrated to their plugin property on the upgrade run. Install steps fire in alphabetical order, so `apps`, `builder`, and `checks` read an app's environment before the `config` plugin had relocated the `ENV` file, found nothing, and moved nothing - without reporting anything. `dokku config:show` kept listing the variable while the plugin behaved as though it were unset, which for `DOKKU_CHECKS_SKIPPED` meant a process type silently regained a health check it was meant to skip. The relocation is now performed before any deprecated variable is read, regardless of install order. Affected installs recover on their next `dokku plugin:install --core`, which the upgrade already runs; the variables listed below can also be re-applied by hand using their replacement command.
- **New in 0.38.27:** The 0.38.26 upgrade merged the leftover pre-0.38.0 `ENV` file back over the current config. Releases 0.38.0 through 0.38.25 recorded the migration and left that file on disk on purpose, and 0.38.26 read it as a hand-edit, so the environment as it stood at the 0.38.x upgrade won over every `dokku config:set` and `dokku config:unset` made since - including reinstating variables and secrets that had been deliberately unset. Nothing appeared broken, because running containers keep the environment they were created with; the rewound values would have reached containers on the next deploy. The 0.38.26 run consumed the leftover file, so this cannot happen a second time, but it also cannot be undone automatically. To check an app that was upgraded to 0.38.26 and has a container still running from before that upgrade, compare each key in `dokku config:show <app>` against `docker exec <container> printenv <key>` - `GIT_REV` is expected to differ after a deploy, anything else that differs was rewound. Repair with `dokku config:set --no-restart <app> KEY=VALUE` for values that regressed and `dokku config:unset --no-restart <app> KEY` for variables that came back.
- The storage plugin now treats persistent volumes as named, scheduler-aware first-class resources via `storage:create`, `storage:mount`, `storage:set`, and `storage:destroy`. The legacy `storage:mount <app> <host>:<container>` colon form continues to work on docker-local apps but is deprecated; on k3s apps it is rejected. Existing colon-form mounts are migrated automatically the first time the new storage plugin runs (during the install trigger) - they appear as `legacy-<hash>` entries in `storage:list-entries`. The migration is idempotent and tied to a per-app flag file at `$DOKKU_LIB_ROOT/config/storage/.migrated/<app>`; deleting that file forces a re-scan on the next install. The `storage:ensure-directory` command keeps working but now emits a deprecation warning - prefer `storage:create <name> [<path>]` (the path defaults to the same `$DOKKU_LIB_ROOT/data/storage/<name>` location). Storage entry names must now be DNS-1123 labels of 45 characters or less so they can be used verbatim as Helm release and Kubernetes resource names; underscores and uppercase characters that the older `ensure-directory` validator accepted are rejected for new names. The migration synthesizer always uses lowercase hex hashes so existing data is never locked out.
### TLS handshake behavior change
With the new catch-all installed, an HTTPS request to a hostname that matches a configured dokku app but where the app has no TLS certificate configured will have its TLS handshake rejected by the catch-all (via `ssl_reject_handshake on`). Previously, nginx fell through to the lexicographically first port-443 server block and presented that block's certificate, producing a cert-mismatch error on the client. The new behavior is a correctness improvement, but operators who deliberately relied on the old fall-through certificate (for monitoring probes, for example) need to either configure a certificate for the target app or remove the catch-all on that host. Existing apps that already have certificates configured are unaffected: nginx selects the right server block via SNI before TLS completion, so the catch-all is never consulted for legitimate requests.
With the new catch-all installed on nginx 1.19.4+, an HTTPS request to a hostname that matches a configured dokku app but where the app has no TLS certificate configured will have its TLS handshake rejected by the catch-all (via `ssl_reject_handshake on`). Previously, nginx fell through to the lexicographically first port-443 server block and presented that block's certificate, producing a cert-mismatch error on the client. The new behavior is a correctness improvement, but operators who deliberately relied on the old fall-through certificate (for monitoring probes, for example) need to either configure a certificate for the target app or remove the catch-all on that host. Existing apps that already have certificates configured are unaffected: nginx selects the right server block via SNI before TLS completion, so the catch-all is never consulted for legitimate requests.
This change does not apply to nginx older than 1.19.4 (e.g., Debian Bullseye's nginx 1.18.0), where the catch-all is installed as an HTTP-only variant. On those systems, HTTPS handshakes to unknown hosts continue to fall through to the first port-443 server block as before.
### Environment variables migrated to plugin properties
@@ -288,3 +288,14 @@ An autoscaling trigger consists of the following properties:
- Example use-cases
- Setting up OAuth clients and DNS
- Loading seed/test data into the app’s test database
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `app-json:report`. The JSON keys emitted by `app-json:report --format json` are the same names with the leading `--app-json-` stripped (e.g. `appjson-path`, `global-appjson-path`, `computed-appjson-path`). Legacy keys with the `app-json-` prefix are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `appjson-path` | app + global | `app.json` | `--app-json-appjson-path`, `--app-json-global-appjson-path`, `--app-json-computed-appjson-path` | Path within the app to the `app.json` manifest, relative to the build root |
@@ -7,3 +7,59 @@ The `nginx.conf.sigil` file is used to configure the nginx server for an applica
A custom `nginx.conf.sigil` is pre-validated at the start of every deploy, immediately after it is extracted from the source tree and before the build phase runs. Pre-validation renders the template via sigil with the same parameters used at deploy time, wraps the rendered config in a minimal `events`/`http` scaffold, and runs `nginx -t` against the result. The deploy is aborted if either the sigil render or the `nginx -t` check fails, so build work is not wasted on a syntactically invalid template. When no app listeners exist yet (typical for first deploys), pre-validation injects a placeholder `127.0.0.1:5000` listener for `DOKKU_APP_WEB_LISTENERS` so that the rendered upstream block has a static server entry and `nginx -t` does not bail out on "host not found in upstream".
Pre-validation is skipped when the proxy type is not `nginx` or when `disable-custom-config` is set to `true` for the app.
### Custom nginx modules
Pre-validation runs `nginx -t` against a minimal wrapper config that does _not_ include the top-level `load_module` directives from the global `/etc/nginx/nginx.conf`. A `nginx.conf.sigil` that uses a directive provided by a dynamically loaded module - such as `image_filter`, provided by the [ngx_http_image_filter_module](https://nginx.org/en/docs/http/ngx_http_image_filter_module.html) - therefore fails pre-validation with an `unknown directive` error, even though `nginx -t` succeeds against the real server config where the module is loaded.
The `load_module` directive cannot be added to the app's `nginx.conf.sigil` to work around this, as that file is included inside the `http { }` block while `load_module` is only valid in nginx's top-level main context.
To make pre-validation aware of a module, override the wrapper template used for validation. The [`nginx-app-template-source`](/docs/development/plugin-triggers.md#nginx-app-template-source) trigger returns the path to the `sigil` template used to generate a given nginx configuration file, and its `validate-config` template type controls the pre-validation wrapper. Create a [custom plugin](/docs/development/plugin-creation.md) that implements the trigger and returns a `validate.conf.sigil` that adds the required `load_module` line at the top of the wrapper.
The trigger file (named `nginx-app-template-source` and marked executable):
The custom `validate.conf.sigil`, which is the [default wrapper](https://github.com/dokku/dokku/blob/master/plugins/nginx-vhosts/templates/validate.conf.sigil) with the required `load_module` line added at the top. Use the same `load_module` line that the host's global `/etc/nginx/nginx.conf` uses:
The same override also governs the standalone `dokku nginx:validate-config` command, which renders the `validate-config` template as well.
## HTTP/2
nginx 1.25.1 deprecated the `http2` parameter on the `listen` directive in favor of a standalone `http2 on;` directive. Custom `nginx.conf.sigil` templates that hardcode `listen ... ssl http2;` will produce `nginx: [warn] the "listen ... http2" directive is deprecated` warnings when run against nginx 1.25.1 or newer.
Dokku exposes the `HTTP2_DIRECTIVE_SUPPORTED` template variable, set to `"true"` when the host nginx is 1.25.1 or newer, so a single template can render the correct syntax against either version. The default template uses this pattern:
Dokku logs a deprecation warning during deploys when a custom template still uses the `listen ... http2` form, so the offending template can be located via the deploy output.
Decoded values are stored byte-for-byte exactly as they were encoded. Be aware that `echo "value" | base64` appends a trailing `\n` before encoding because `echo` always adds a newline, and that newline becomes part of the stored value. This is rarely what you want for short inline values - use `printf '%s'` or `echo -n` instead:
When setting or unsetting environment variables, you may wish to avoid an application restart. This is useful when developing plugins or when setting multiple environment variables in a scripted manner. To do so, use the `--no-restart` flag:
```shell
@@ -129,3 +142,13 @@ The following config variables have special meanings and can be set in a variety
| `DOKKU_TRACE` | none | `dokku trace:on` <br /> `dokku trace:off` <br /> `--trace` flag | Turn on very verbose debugging. |
| `DOKKU_SYSTEM_GROUP` | `dokku` | `/etc/environment` <br /> `~dokku/.dokkurc` <br /> `~dokku/.dokkurc/*` | System group to chown files as. |
| `DOKKU_SYSTEM_USER` | `dokku` | `/etc/environment` <br /> `~dokku/.dokkurc` <br /> `~dokku/.dokkurc/*` | System user to chown files as. |
## Properties
### Internal properties
The following property is recorded internally by the config plugin and is not exposed via `config:report`:
| Property | Description | Source |
|---|---|---|
| `env-migrated` | Migration sentinel that records that the per-app or global `ENV` file has been drained out of its pre-0.38.0 location into the config path. The drain happens once: a file found at the old path once this is recorded is never imported, since the config path holds every change made since. Such a file is removed when it agrees with the current config, and otherwise moved aside to `ENV.migrated` with the keys it disagrees on named in a warning. The preserved copy is left for review and should be deleted afterwards, since it holds keys that were unset on purpose | `plugins/config/migrate.go` writes `"true"` once the file has been drained |
> Archives passed to `certs:add` are validated before extraction to prevent path traversal and symlink escape attacks. Archives containing absolute paths, parent directory traversal entries, or symlinks pointing outside the extraction directory will be rejected.
#### SSL and Multiple Domains
When an SSL certificate is associated to an application, the certificate will be associated with _all_ domains currently associated with said application. Your certificate _should_ be associated with all of those domains, otherwise accessing the application will result in SSL errors. If you wish to remove one of the domains from the application, refer to the [domain configuration documentation](/docs/configuration/domains.md).
@@ -140,8 +143,27 @@ Certain versions of nginx have bugs that prevent [HTTP/2](https://nginx.org/en/d
When your app is served from port `80` then the `/home/dokku/APP/nginx.conf` file will automatically be updated to instruct nginx to respond to ssl on port 443 as a new cert is added. If your app uses a non-standard port (perhaps you have a dockerfile deploy exposing port `99999`) you may need to manually expose an ssl port via `dokku ports:add <APP> https:443:99999`.
If an `https:443:*` port mapping already exists when a certificate is installed or renewed, Dokku will preserve it rather than rewriting it from the `http:80:*` mappings. This allows apps that terminate TLS inside the container (for example, with a configuration like `http:80:80 https:443:443`) to keep their explicit mapping across `dokku certs:add` and unattended renewals.
## Other
### Running behind a proxy (`X-Forwarded-Ssl`, etc.)
See the [running behind another proxy documentation](/docs/networking/proxies/nginx.md#running-behind-another-proxy--configuring-x-forwarded--headers) for more information on how to configure your Nginx config when your server is running behind a proxy (e.g. load balancer, etc.).
## Properties
### Read-only flags
The following flags surface in `certs:report` but are not managed by `certs:set` - they are derived from the on-disk certificate:
| Flag | Description |
|---|---|
| `--ssl-enabled` | `true` when an SSL certificate is installed for the app |
| `--ssl-dir` | Absolute path to the per-app certificate directory |
| `--ssl-hostnames` | Hostnames the certificate covers (CN plus Subject Alternative Names) |
| `--ssl-issuer` | Certificate issuer DN |
| `--ssl-subject` | Certificate subject DN |
| `--ssl-verified` | `true` if the certificate chain verifies against the system CA bundle |
apps:locked <app> # Checks if an app is locked for deployment
apps:rename <old-app> <new-app> # Rename an app
@@ -47,6 +47,18 @@ node-js-app
python-app
```
You can also retrieve the list of apps as a JSON array by using the `--format json` flag, which is useful for programmatic consumption:
```shell
dokku apps:list --format json
```
```json
["node-js-app","python-app"]
```
When no apps exist, the JSON output is an empty array (`[]`).
### Checking if an application exists
For CI/CD pipelines, it may be useful to see if an application exists before creating a "review" application for a specific branch. You can do so via the `apps:exists` command:
@@ -312,3 +324,26 @@ You can pass flags which will output only the value of the specific information
```shell
dokku apps:report node-js-app --app-dir
```
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `apps:report`. The JSON keys emitted by `apps:report --format json` are the same names with the leading `--app-` stripped (e.g. `global-disable-autocreation`). Legacy keys with the `app-` prefix (e.g. `app-global-disable-autocreation`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `disable-autocreation` | global only | `false` | `--app-global-disable-autocreation` | When `true`, pushes to a remote for an app that does not yet exist are rejected instead of auto-creating the app |
### Read-only flags
The following flags surface in `apps:report` but are not managed by `apps:set`:
| Flag | Description |
|---|---|
| `--app-created-at` | UNIX timestamp of app creation |
| `--app-deploy-source` | Source kind of the last deploy (`git`, `archive`, `docker-image`, `git-sync`) |
| `--app-deploy-source-metadata` | Free-form metadata for the deploy source (commit sha, image ref, URL) |
| `--app-dir` | Absolute path of the app root on disk |
| `--app-locked` | `true` while a deploy or rebuild holds the app lock |
@@ -172,3 +172,24 @@ Custom plugins names _must_ have the prefix `builder-` or builder overriding via
Builders can use any tools available on the system to build the docker image, and may even be used to schedule building off-server. The only current requirement is that the image must exist on the server at the end of the `builder-build` command, though this requirement may be relaxed in a future release.
For a simple example of how to implement this trigger, see `builder-pack`, which utilizes a cli tool - `pack-cli` - to generate an OCI image that is compatible with Docker and can be scheduled by the official scheduling plugins.
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `builder:report`. The JSON keys emitted by `builder:report --format json` are the same names with the leading `--builder-` stripped (e.g. `selected`, `global-selected`, `computed-selected`). Legacy keys with the `builder-` prefix are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `build-dir` | app + global | none | `--builder-build-dir`, `--builder-global-build-dir`, `--builder-computed-build-dir` | Subdirectory within the repository to use as the build context |
| `skip-cleanup` | app + global | `false` | `--builder-skip-cleanup`, `--builder-global-skip-cleanup`, `--builder-computed-skip-cleanup` | When `true`, leaves intermediate build artifacts in place after the build |
### Read-only flags
The following flags surface in `builder:report` but are not managed by `builder:set`:
| Flag | Description |
|---|---|
| `--builder-detected` | Builder auto-selected for the app when `selected` is unset |
To replace the complete ordered buildpack list in a single command, use the `--replace` flag. This is useful for tooling that needs to apply a full buildpack list atomically instead of running `buildpacks:clear` followed by multiple `buildpacks:add` calls.
The buildpacks are executed in the order they are specified, and the previous list is discarded. If any specified buildpack is invalid, the existing list is left unchanged.
A single buildpack may also be specified to replace the entire list with just that buildpack:
> The `--replace` flag cannot be combined with the `--index` flag. To remove all buildpacks, use the `buildpacks:clear` command instead of `--replace` with no buildpacks.
### Removing a buildpack
> At least one of a buildpack or index must be specified
@@ -190,3 +210,16 @@ You can pass flags which will output only the value of the specific information
These properties are managed via `buildpacks:set-property` (the legacy command name for this plugin).
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `buildpacks:report`. The JSON keys emitted by `buildpacks:report --format json` are the same names with the leading `--buildpacks-` stripped (e.g. `stack`, `global-stack`, `computed-stack`). Legacy keys with the `buildpacks-` prefix are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
Builder pack global projecttoml path: project.json
Builder pack global projecttoml path:
Builder pack projecttoml path:
```
The `projecttoml-path` and `global-projecttoml-path` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-projecttoml-path` key holds the effective value used at build time, falling back to the global value (where one has been set) and then to the built-in default of `project.toml`.
See the [Procfile documentation](/docs/processes/process-management.md#procfile) for more information on how to specify different processes for your app.
| `projecttoml-path` | app + global | `project.toml` | `--builder-pack-projecttoml-path`, `--builder-pack-global-projecttoml-path`, `--builder-pack-computed-projecttoml-path` | Path within the app to the CNB `project.toml` manifest, relative to the build root |
Builder dockerfile global dockerfile path: Dockerfile
Builder dockerfile global dockerfile path:
Builder dockerfile dockerfile path:
```
The `dockerfile-path` and `global-dockerfile-path` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-dockerfile-path` key holds the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default of `Dockerfile`.
Builder dockerfile global dockerfile path: Dockerfile
Builder dockerfile global dockerfile path:
Builder dockerfile dockerfile path: Dockerfile2
```
@@ -213,3 +215,11 @@ See the [Procfile documentation](/docs/processes/process-management.md#procfile)
### Exposed ports
See the [port management documentation](/docs/networking/port-management.md) for more information on how Dokku exposes ports for applications and how you can configure these for your app.
| `dockerfile-path` | app + global | `Dockerfile` | `--builder-dockerfile-dockerfile-path`, `--builder-dockerfile-global-dockerfile-path`, `--builder-dockerfile-computed-dockerfile-path` | Path within the app to the Dockerfile used by the dockerfile builder |
=====> python-sample builder-herokuish information
Builder herokuish computed allowed: true
Builder herokuish global allowed: true
Builder herokuish global allowed:
Builder herokuish allowed:
=====> ruby-sample builder-herokuish information
Builder herokuish computed allowed: true
Builder herokuish global allowed: true
Builder herokuish global allowed:
Builder herokuish allowed:
```
The `allowed` and `global-allowed` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-allowed` key holds the effective value used at build time, falling back to the global value (where one has been set) and then to the built-in default (`true` on amd64, `false` otherwise).
@@ -183,29 +185,37 @@ See the [Procfile documentation](/docs/processes/process-management.md#procfile)
### Listing Buildpacks in Use
See the [buildpack management documentation](/docs/processes/process-management.md#listing-buildpacks-in-use) for more information on how to list buildpacks in use.
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#listing-buildpacks-in-use) for more information on how to list buildpacks in use.
### Adding custom buildpacks
See the [buildpack management documentation](/docs/processes/process-management.md#adding-custom-buildpacks) for more information on how to add custom buildpacks.
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#adding-custom-buildpacks) for more information on how to add custom buildpacks.
### Overwriting a buildpack position
See the [buildpack management documentation](/docs/processes/process-management.md#overwriting-a-buildpack-position) for more information on how to overwrite a buildpack position.
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#overwriting-a-buildpack-position) for more information on how to overwrite a buildpack position.
### Removing a buildpack
See the [buildpack management documentation](/docs/processes/process-management.md#removing-a-buildpack) for more information on how to remove a buildpack.
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#removing-a-buildpack) for more information on how to remove a buildpack.
### Clearing all buildpacks
See the [buildpack management documentation](/docs/processes/process-management.md#clearing-all-buildpacks) for more information on how to clear all buildpacks.
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#clearing-all-buildpacks) for more information on how to clear all buildpacks.
### Using a specific buildpack version
See the [buildpack management documentation](/docs/processes/process-management.md#using-a-specific-buildpack-version) for more information on how to using a specific buildpack version
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#using-a-specific-buildpack-version) for more information on how to using a specific buildpack version
### Displaying buildpack reports for an app
See the [buildpack management documentation](/docs/processes/process-management.md#displaying-buildpack-reports-for-an-app) for more information on how to display buildpack reports for an app.
See the [buildpack management documentation](/docs/deployment/builders/buildpack-management.md#displaying-buildpack-reports-for-an-app) for more information on how to display buildpack reports for an app.
| `allowed` | app + global | `true` | `--builder-herokuish-allowed`, `--builder-herokuish-global-allowed`, `--builder-herokuish-computed-allowed` | When `false`, the herokuish builder is skipped during builder detection for this app |
The `lambdayml-path` and `global-lambdayml-path` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-lambdayml-path` key holds the effective value used at build time, falling back to the global value (where one has been set) and then to the built-in default of `lambda.yml`.
| `lambdayml-path` | app + global | `lambda.yml` | `--builder-lambda-lambdayml-path`, `--builder-lambda-global-lambdayml-path`, `--builder-lambda-computed-lambdayml-path` | Path within the app to the `lambda.yml` manifest used by the lambda builder |
Builder-nixpacks global nixpackstoml path: nixpacks.toml
Builder-nixpacks global nixpackstoml path:
Builder-nixpacks nixpackstoml path:
```
The `nixpackstoml-path` and `global-nixpackstoml-path` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-nixpackstoml-path` key holds the effective value used at build time, falling back to the global value (where one has been set) and then to the built-in default of `nixpacks.toml`.
| `nixpackstoml-path` | app + global | `nixpacks.toml` | `--builder-nixpacks-nixpackstoml-path`, `--builder-nixpacks-global-nixpackstoml-path`, `--builder-nixpacks-computed-nixpackstoml-path` | Path within the app to the `nixpacks.toml` manifest used by the nixpacks builder |
Builder-railpack global railpackjson path: railpack.json
Builder-railpack global railpackjson path:
Builder-railpack railpackjson path:
```
The `railpackjson-path` and `global-railpackjson-path` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-railpackjson-path` key holds the effective value used at build time, falling back to the global value (where one has been set) and then to the built-in default of `railpack.json`.
| `railpackjson-path` | app + global | `railpack.json` | `--builder-railpack-railpackjson-path`, `--builder-railpack-global-railpackjson-path`, `--builder-railpack-computed-railpackjson-path` | Path within the app to the `railpack.json` manifest used by the railpack builder |
@@ -124,6 +124,21 @@ The `/etc/vector` mount includes the `vector.json` configuration file, but also
The final volume mount - `/var/log/dokku/apps` - may be used for users that wish to ship logs to a file on disk that may be later logrotated. This directory is owned by the `dokku` user and group, with permissions set to `0755`. At this time, log-rotation is not configured for this directory.
Operators using a `file` sink are encouraged to configure rotation themselves, as Dokku will not truncate these files. A minimal `/etc/logrotate.d/dokku-app-logs` might look like:
```
/var/log/dokku/apps/*/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
```
`copytruncate` is used because Vector holds the file open between writes.
#### Stopping the Vector container
Vector may be stopped via the `logs:vector-stop` command.
@@ -178,6 +193,39 @@ Setting this to an empty string will reset the version to the version currently
dokku logs:set --global vector-image
```
#### Attaching Vector to additional Docker networks
By default, the Vector container runs with `network_mode: bridge` and can only reach app containers that are also on the default bridge network. Apps deployed onto a per-app network or a custom network - typically via `dokku network:set <app> initial-network <name>` - are not reachable from Vector over Docker's internal DNS, so sinks that need to talk to those apps directly (for example, an in-host log search service such as [Logpond](https://github.com/dokku/logpond)) would have to route traffic out through the external proxy.
The global `vector-networks` property accepts a comma-separated list of Docker networks for Vector to join.
Setting this property **replaces** the default bridge attachment: the Vector container will be on the configured user-defined networks only, not the default Docker `bridge` network. Outbound traffic continues to work through the user-defined networks' NAT, so external sinks such as Datadog or hosted HTTP endpoints remain reachable.
Each network must already exist; setting a non-existent network or the reserved `bridge` value will fail. The list can be cleared by setting an empty value, which restores the default `network_mode: bridge` configuration.
```shell
dokku logs:set --global vector-networks
```
Network attachments are reconciled by `docker compose` on every `logs:vector-start`, so after changing the value the Vector container must be cycled.
```shell
dokku logs:vector-stop
dokku logs:vector-start
```
Once attached, an app on `dokku-logs` (for example via `dokku network:set node-js-app initial-network dokku-logs`) is reachable from Vector at `<app>.<process>:<port>` over the shared network without round-tripping through the external proxy.
#### Configuring a log sink
Vector uses the concept of log "sinks" to send logs to a given endpoint. Log sinks may be configured globally or on a per-app basis by specifying a `vector-sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container.
@@ -208,6 +256,8 @@ As with app-specific sink settings, the global value may also be cleared by sett
dokku logs:set --global vector-sink
```
The generated vector configuration is also rewritten whenever an app is renamed, cloned or destroyed. A renamed app keeps shipping to its sink under the new name, a cloned app gets a source of its own for the sink it inherited, and a destroyed app's source and sink are removed rather than left pointing at an endpoint that was decommissioned with the app.
##### Log Sink DSN Format
The DSN form of a sink is as follows:
@@ -249,28 +299,85 @@ This will transform the value to it's encoded form when configuring Vector sinks
Please read the [sink documentation](https://vector.dev/docs/reference/configuration/sinks/) for your sink of choice to configure the sink as desired.
##### Configuring the app label
#### Configuring a cron task log sink
Logs shipped by vector include the label `com.dokku.app-name`, which is an alias for the app name. This can be changed via the `app-label-alias` logs property with the `logs:set` command. Specifying a new alias will reload any running vector container.
Scheduled cron tasks run in one-off containers that carry the app's usual labels, so their output is already collected by the `vector-sink` configured for the app or globally. To send that output somewhere separate, set a `vector-cron-sink`.
```shell
# setting the sink value in quotes is encouraged to avoid
# issues with ampersand encoding in shell commands
Setting a cron sink **moves** cron task output rather than copying it. Vector routes each log line to exactly one of the two sinks:
| Configuration | Where cron output goes | Where all other output goes |
|---|---|---|
| `vector-sink` only | `vector-sink` | `vector-sink` |
| `vector-cron-sink` only | `vector-cron-sink` | nowhere |
| both | `vector-cron-sink` | `vector-sink` |
If an app is already shipping to a metered service via `vector-sink`, adding a cron sink will stop cron output from arriving there.
Events on the cron branch have two extra fields added to them, so that they can be used in sink options that support templating:
-`dokku_app`: the name of the app the task belongs to
-`dokku_cron_id`: the cron task ID, as shown by `dokku cron:list`
> [!WARNING]
> Cron task containers are removed as soon as the task exits. Vector attaches to a container after it starts, so output from tasks that finish almost immediately - a bare `echo`, for instance - may be missed. Log shipping should not be relied on as the sole record that a task ran; use an external check for that.
##### Writing cron output to a file on disk
The `file` sink writes to a path within the vector container. The `/var/log/dokku/apps` directory is mounted into that container from the host at the same path, so it is the correct destination for output that should survive on the host.
Quoting the value is required, both for the `&` separators and for the spaces inside the template.
> [!WARNING]
> Vector drops any event whose templated `path` references a field it cannot resolve. Only `dokku_app` and `dokku_cron_id` are guaranteed to exist on cron events - referencing anything else risks silently discarding log lines.
Vector creates missing parent directories, and buffers writes before flushing. Set `idle_timeout_secs` to shorten that delay for infrequent tasks:
Dokku labels every app container with `com.dokku.app-name`, and events shipped by vector carry that label as the field `label."com.dokku.app-name"`. Some sinks cannot use a field named that way - Loki label names, for instance, may only contain letters, digits and underscores - so the field can be renamed on the way to the sink via the `app-label-alias` logs property. Specifying a new alias will reload any running vector container.
Events for `node-js-app` then carry `label.app_name` and no longer carry `label."com.dokku.app-name"`.
An alias may be removed by setting an empty value, which will also reload the running vector container.
```shell
dokku logs:set node-js-app app-label-alias
```
Only one alias may be specified on a per-app basis at a given time.
Only one alias may be specified on a per-app basis at a given time. Valid values start with a letter or number and may otherwise contain letters, numbers, underscores, periods and hyphens.
App label aliases can also be specified globally by specifying the `--global` flag to `logs:set` with no app name specified:
As with app-specific label alias settings, the global value may also be cleared by setting no value.
@@ -278,3 +385,23 @@ As with app-specific label alias settings, the global value may also be cleared
```shell
dokku logs:set --global app-label-alias
```
An app-specific value takes precedence over the global one, and is applied to that app's events whether they are shipped by the app's own `vector-sink` or by the global one.
The alias only changes the shipped event. Containers are always discovered by the `com.dokku.app-name` label, so changing this property never affects which logs are collected, and a change takes effect on the next vector reload without redeploying the app. Cron events are unaffected in another respect too: `dokku_app` is read from the container label before the rename, so it holds the app name regardless of the configured alias.
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `logs:report`. The JSON keys emitted by `logs:report --format json` are the same names with the leading `--logs-` stripped (e.g. `max-size`, `global-max-size`, `computed-max-size`). Legacy keys with the `logs-` prefix are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `app-label-alias` | app + global | `com.dokku.app-name` | `--logs-app-label-alias`, `--logs-global-app-label-alias`, `--logs-computed-app-label-alias` | Field name the app name is shipped under, renamed from `com.dokku.app-name` on the event |
| `max-size` | app + global | `10m` | `--logs-max-size`, `--logs-global-max-size`, `--logs-computed-max-size` | Maximum size of an individual log file before rotation |
| `vector-image` | global only | _parsed from `plugins/logs/Dockerfile`_ | `--logs-global-vector-image`, `--logs-computed-vector-image` | Docker image used to run the vector log-shipper container |
| `vector-networks` | global only | none | `--logs-global-vector-networks`, `--logs-computed-vector-networks` | Comma-separated list of docker networks the vector container is attached to |
| `vector-cron-sink` | app + global | none | `--logs-vector-cron-sink`, `--logs-global-vector-cron-sink`, `--logs-computed-vector-cron-sink` | DSN-style sink configuration for scheduled cron task output; when set, cron output is routed here instead of to `vector-sink` |
| `vector-sink` | app + global | none | `--logs-vector-sink`, `--logs-global-vector-sink`, `--logs-computed-vector-sink` | DSN-style sink configuration for vector (e.g. `console://` or `loki://...`) |
Archive contents are validated before extraction to prevent path traversal and symlink escape attacks. Archives containing absolute paths, parent directory traversal entries (`..`), or symlinks pointing outside the extraction directory are rejected.
The following limits can be configured via global git properties:
-`archive-max-size` - maximum archive size in bytes (default: `1073741824`, 1 GiB)
-`archive-max-files` - maximum number of entries in an archive (default: `10000`)
The bare per-app keys (`deploy-branch`, `keep-git-dir`) and the `global-<prop>` keys hold the raw per-app or global value respectively, and are empty when nothing has been set. The `computed-<prop>` keys hold the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default.
| `archive-max-files` | app + global | none | `--git-global-archive-max-files`, `--git-computed-archive-max-files` | Maximum number of files allowed in an uploaded archive deploy |
| `archive-max-size` | app + global | none | `--git-global-archive-max-size`, `--git-computed-archive-max-size` | Maximum total size of an uploaded archive deploy |
| `deploy-branch` | app + global | `master` | `--git-deploy-branch`, `--git-global-deploy-branch`, `--git-computed-deploy-branch` | Branch name pushed by the git remote that triggers a deploy |
| `keep-git-dir` | app + global | `false` | `--git-keep-git-dir`, `--git-global-keep-git-dir`, `--git-computed-keep-git-dir` | When `true`, retains the `.git` directory inside the build context |
| `rev-env-var` | app + global | `GIT_REV` | `--git-rev-env-var` | Environment variable name receiving the deployed commit SHA; empty disables injection |
| `source-image` | app + global | none | `--git-source-image` | Docker image to clone application source from when deploying from an image |
### Read-only flags
The following flags surface in `git:report` but are not managed by `git:set` - they are derived from repository state:
| Flag | Description |
|---|---|
| `--git-sha` | HEAD commit SHA of the app's git repo |
| `--git-last-updated-at` | UNIX timestamp of the last write to the deploy branch ref |
All image containers with the label `org.opencontainers.image.vendor=linuxserver.io` will have the automatic init process injection force-disabled without further intervention.
The default value may also be configured globally with the `--global` flag. Per-app values take precedence over the global value when set.
All image containers with the label `org.opencontainers.image.vendor=linuxserver.io` will have the automatic init process injection force-disabled without further intervention when neither an app-level nor a global value is set.
### Deploying Process Types in Parallel
@@ -66,6 +78,18 @@ Once set, you may reset it by setting a blank value for `parallel-schedule-count
If the value of `parallel-schedule-count` is increased and a given process type fails to schedule successfully, then any in-flight process types will continue to be processed, while all process types that have not been scheduled will be skipped before the deployment finally fails.
Container scheduling output is shown in the order it is received, and thus may be out of order in case of output to stderr.
@@ -100,6 +124,57 @@ Note that increasing the value of `max_parallel` may significantly impact CPU ut
See the [app.json location documentation](/docs/advanced-usage/deployment-tasks.md#changing-the-appjson-location) for more information on where to place your `app.json` file.
### Displaying scheduler-docker-local reports for an app
You can get a report about the app's scheduler-docker-local configuration using the `scheduler-docker-local:report` command:
```shell
dokku scheduler-docker-local:report
```
```
=====> node-js-app scheduler-docker-local information
Scheduler docker local computed init process: true
Scheduler docker local computed parallel schedule count:1
Scheduler docker local global init process: true
Scheduler docker local global parallel schedule count: 1
Scheduler docker local init process:
Scheduler docker local parallel schedule count:
```
You can run the command for a specific app also.
```shell
dokku scheduler-docker-local:report node-js-app
```
You can pass flags which will output only the value of the specific information you want. For example:
When run against `--global`, only the global keys are reported.
```shell
dokku scheduler-docker-local:report --global
```
The available keys are:
-`--scheduler-docker-local-init-process`: the raw per-app init-process value (empty when unset).
-`--scheduler-docker-local-computed-init-process`: the effective init-process value, computed as the per-app value if set, otherwise the global value, otherwise `true`.
-`--scheduler-docker-local-global-init-process`: the global init-process value (defaults to `true`).
-`--scheduler-docker-local-parallel-schedule-count`: the raw per-app parallel-schedule-count value (empty when unset).
-`--scheduler-docker-local-computed-parallel-schedule-count`: the effective parallel-schedule-count value, computed as the per-app value if set, otherwise the global value, otherwise `1`.
-`--scheduler-docker-local-global-parallel-schedule-count`: the global parallel-schedule-count value (defaults to `1`).
The report may also be emitted as JSON using `--format json`, which is useful for external tooling that needs to distinguish a value set on the app from one that is defaulting.
The following sections describe implemented scheduler functionality for the `docker-local` scheduler.
@@ -141,3 +216,12 @@ The `docker-local` scheduler supports a minimal list of resource _limits_ and _r
- memory: (docker option: `--memory-reservation`) should be specified with a suffix of `b` (bytes), `k` (kilobytes), `m` (megabytes), `g` (gigabytes). Default unit is `m` (megabytes).
- See the ["Memory" section](https://docs.docker.com/config/containers/resource_constraints/#memory) of the Docker Runtime Options documentation for more information.
| `init-process` | app + global | `true` | `--scheduler-docker-local-init-process`, `--scheduler-docker-local-global-init-process`, `--scheduler-docker-local-computed-init-process` | When `true`, runs containers with Docker's `--init` flag (reaping zombie processes) |
| `parallel-schedule-count` | app + global | `1` | `--scheduler-docker-local-parallel-schedule-count`, `--scheduler-docker-local-global-parallel-schedule-count`, `--scheduler-docker-local-computed-parallel-schedule-count` | Maximum number of containers scheduled in parallel during a deploy |
scheduler-k3s:annotations:set <app|--global> <property> (<value>) [--process-type PROCESS_TYPE] <--resource-type RESOURCE_TYPE>, Set or clear an annotation for a given app/process-type/resource-type combination
scheduler-k3s:autoscaling-auth:set <app|--global> <trigger> [<--metadata key=value>...], Set or clear a scheduler-k3s autoscaling keda trigger authentication resource for an app
scheduler-k3s:autoscaling-auth:report <app|--global> [--format stdout|json] [--include-metadata] # Displays a scheduler-k3s autoscaling auth report for an app
scheduler-k3s:cluster:add [ssh://user@host:port] # Adds a server node to a Dokku-managed cluster
scheduler-k3s:cluster:list # Lists all nodes in a Dokku-managed cluster
scheduler-k3s:cluster:remove [node-id] # Removes client node to a Dokku-managed cluster
scheduler-k3s:ensure-charts # Ensures the k3s charts are installed
scheduler-k3s:initialize # Initializes a cluster
scheduler-k3s:annotations:set <app|--global> <property> (<value>) [--process-type PROCESS_TYPE] <--resource-type RESOURCE_TYPE> # Set or clear an annotation for a given app/process-type/resource-type combination
scheduler-k3s:annotations:report [<app>|--global] [--format stdout|json] [--process-type PROCESS_TYPE] [--resource-type RESOURCE_TYPE] # Displays a scheduler-k3s annotations report for one or more apps
scheduler-k3s:autoscaling-auth:set <app|--global> <trigger> [<--metadata key=value>...] # Set or clear a scheduler-k3s autoscaling keda trigger authentication resource for an app
scheduler-k3s:autoscaling-auth:report [<app>|--global] [--format stdout|json] [--include-metadata] # Displays a scheduler-k3s autoscaling auth report for one or more apps
scheduler-k3s:charts:set <chart-name.property> (<value>) # Set or clear a chart-specific helm value
scheduler-k3s:cluster:add [--profile PROFILE] [--role ROLE] [--insecure-allow-unknown-hosts] [--server-ip SERVER_IP] [--taint-scheduling] [--kubelet-args KUBELET_ARGS] <ssh://user@host:port> # Adds a server node to a Dokku-managed cluster
scheduler-k3s:cluster:list [--format json|stdout] # Lists all nodes in a Dokku-managed cluster
scheduler-k3s:cluster:remove [node-id] # Removes client node to a Dokku-managed cluster
scheduler-k3s:ensure-charts # Ensures the k3s charts are installed
scheduler-k3s:initialize [--server-ip SERVER_IP] [--taint-scheduling] [--kubelet-args KUBELET_ARGS] # Initializes a cluster
scheduler-k3s:labels:set <app|--global> <property> (<value>) [--process-type PROCESS_TYPE] <--resource-type RESOURCE_TYPE> # Set or clear a label for a given app/process-type/resource-type combination
scheduler-k3s:profiles:add <profile> [--role ROLE] [--insecure-allow-unknown-hosts] [--taint-scheduling] [--kubelet-args KUBELET_ARGS] Adds a node profile to the k3s cluster
scheduler-k3s:profiles:list [--format json|stdout] # Lists all node profiles in the k3s cluster
scheduler-k3s:profiles:remove <profile> # Removes a node profile from the k3s cluster
scheduler-k3s:report [<app>] [<flag>] # Displays a scheduler-k3s report for one or more apps
scheduler-k3s:set [<app>|--global] <key> (<value>) # Set or clear a scheduler-k3s property for an app or the scheduler
scheduler-k3s:show-kubeconfig # Displays the kubeconfig for remote usage
scheduler-k3s:uninstall # Uninstalls k3s from the Dokku server
scheduler-k3s:labels:report [<app>|--global] [--format stdout|json] [--process-type PROCESS_TYPE] [--resource-type RESOURCE_TYPE] # Displays a scheduler-k3s labels report for one or more apps
scheduler-k3s:node-sysctls:set <sysctl> (<value>) [--global|--profile PROFILE] # Set or clear a node-level kernel sysctl for unprofiled nodes or a single node profile
scheduler-k3s:node-sysctls:report [--format stdout|json] # Displays the node-level kernel sysctls applied to each scope
scheduler-k3s:preview <app> [--context N] [--show-secrets] [--show-secrets-decoded] # Displays a diff between the current and next deployment for an app
scheduler-k3s:profiles:add <profile> [--role ROLE] [--insecure-allow-unknown-hosts] [--taint-scheduling] [--kubelet-args KUBELET_ARGS] # Adds a node profile to the k3s cluster
scheduler-k3s:profiles:list [--format json|stdout] # Lists all node profiles in the k3s cluster
scheduler-k3s:profiles:remove <profile> # Removes a node profile from the k3s cluster
scheduler-k3s:report [<app>|--global] [--format stdout|json] [<flag>] # Displays a scheduler-k3s report for one or more apps
scheduler-k3s:set <app|--global> <property> (<value>) # Set or clear a scheduler-k3s property for an app or globally
scheduler-k3s:show-kubeconfig # Displays the kubeconfig for remote usage
scheduler-k3s:uninstall # Uninstalls k3s from the Dokku server
```
> [!NOTE]
@@ -73,6 +80,20 @@ Dokku can also use Traefik on cluster initialization via the [Traefik's CRDs](ht
Kubelet flags for the initial server node can be supplied by passing `--kubelet-args` with a comma-separated `key=value` list. This is the only way to configure the kubelet on the node created by `scheduler-k3s:initialize`, as that node never passes through `scheduler-k3s:cluster:add`.
Removal only deletes the stored definition; nodes that already joined the cluster keep their existing configuration.
#### The node profile label
When a node joins via `scheduler-k3s:cluster:add --profile <name>`, Dokku labels it with `dokku.com/node-profile=<name>`. This makes a profile selectable after the fact, whether via `kubectl`, a `nodeSelector`, or a node affinity rule.
```shell
kubectl get nodes -L dokku.com/node-profile
```
Nodes added without `--profile` are not labeled, as an empty label value would be indistinguishable from a profile literally named the empty string.
Two limits are worth knowing before relying on this label:
- The server node never carries it. That node is created by `scheduler-k3s:initialize` and never passes through `scheduler-k3s:cluster:add`, so no profile is ever associated with it.
- Nodes that joined before this label existed are not backfilled. Use `kubectl label node <node> dokku.com/node-profile=<name>` to set it on an existing node.
### Changing deployment settings
The k3s plugin provides a number of settings that can be used to managed deployments on a per-app basis. The following table outlines ones not covered elsewhere:
@@ -232,6 +268,74 @@ The global default value may be set by passing an empty value for the option.
dokku scheduler-k3s:set --global deploy-timeout
```
### Restarting apps
A `ps:restart` re-renders the app's Helm chart from its current configuration and upgrades the release, which is how configuration changes are picked up. Pods cycle because each Deployment's pod template carries an `app.kubernetes.io/version` annotation that changes on restart.
A single process type may be targeted, in which case only that process type's pods are replaced. The rest of the release is still upgraded so configuration converges everywhere, but the untargeted Deployments keep their existing annotation and are left running:
```shell
dokku ps:restart node-js-app web
```
The app image does not need to be present on the Dokku host. Kubernetes pulls it from the registry, so a host that has reaped its local copy - as the `registry` plugin does on its own once an app has been deployed a number of times - can still restart, scale, and run one-off commands against the app. The builder type and working directory needed to render the chart are read back from the app's current Helm release when the image is unavailable locally.
There is one exception. An `app.json` with a `scripts.dokku.postdeploy` task runs that task in a container on the Dokku host rather than in the cluster, and so does require the image locally. Apps without a postdeploy task are unaffected.
### Displaying the scheduler report
Configured properties can be inspected with the `scheduler-k3s:report` command. Without arguments, it iterates every app. Passing an app name scopes the report to that app, while `--global` reports the scheduler-wide properties on their own:
```shell
dokku scheduler-k3s:report
dokku scheduler-k3s:report node-js-app
dokku scheduler-k3s:report --global
```
The output can be emitted as JSON for programmatic consumption:
```shell
dokku scheduler-k3s:report --global --format json
```
A single value can also be read directly by passing its report flag, as listed in the [settable properties](#settable-properties) table:
Before triggering a deploy, the `scheduler-k3s:preview` command can be used to display a unified diff between the manifests currently stored in the live Helm release for an app and the manifests that the next deploy would roll out:
```shell
dokku scheduler-k3s:preview node-js-app
```
A clean redeploy with no property changes produces no output. After modifying a property that affects the main app chart (for example `dokku resource:limit --memory 256m node-js-app`), the command shows the unified diff for each changed resource. For an app that has never been deployed, the entire proposed manifest is rendered as added lines.
By default each change is shown with 3 lines of surrounding context, git-diff style. The `--context` flag overrides this; pass `-1` to render the full resource around every change:
By default any `kind: Secret` resources have their `data` values redacted so secret bytes never appear in terminal scrollback or CI logs. Two flags adjust this behavior:
`--show-secrets` keeps the raw base64-encoded values, and `--show-secrets-decoded` renders them base64-decoded. In current dokku, the only Secret objects in the main app chart come from KEDA autoscaling auth resources configured via [`scheduler-k3s:autoscaling-auth:set`](#workload-autoscaling-authentication); for apps without autoscaling auth, these flags are no-ops.
> [!IMPORTANT]
> The preview only covers the main app helm release (the release named after the app). Dokku installs three auxiliary helm releases per app for config-var storage, image-pull credentials, and TLS certificates, and changes to those releases do **not** appear in this preview. In particular, a `dokku config:set` change followed by `scheduler-k3s:preview` will show no diff because the new config-var value lives in the separate `config-<app>` helm release.
> [!NOTE]
> Helm renders the proposed manifest using a client-side dry-run. The `lookup` template function returns empty values in this mode, so charts overridden via `scheduler-k3s:charts:set` that depend on `lookup` may render differently here than at actual deploy time. Dokku's bundled chart templates do not use `lookup`.
### Exposing services on the network
Dokku will automatically expose the `web` process as a Kubernetes Service, with all others being treated as background processes. In some cases, it may be useful to have other processes exposed as Kubernetes Service objects so as to segregate internal http endpoints from public http endpoints. This can be done by modifying the `app.json` Formation entry for your process type.
@@ -253,6 +357,20 @@ In the above example, the `internal-web` process is exposed as a service. The `P
> [!NOTE]
> It is not possible to modify the port mapping, nor is it possible to assign domains or SSL to a non-web process.
### Wildcard domains
Both the `nginx` and `traefik` ingress classes route wildcard domains. Add the wildcard as a domain on the app:
```shell
dokku domains:add node-js-app '*.node-js-app.com'
```
A wildcard matches exactly one label, matching DNS itself, so `*.node-js-app.com` covers `api.node-js-app.com` but not `node-js-app.com` or `api.staging.node-js-app.com`. Add the apex as a separate domain if it should also be served.
An exact domain always takes precedence over a wildcard, including across apps. If one app serves `*.node-js-app.com` and another serves `api.node-js-app.com`, requests for `api.node-js-app.com` are routed to the second app.
Only the leading label may be wildcarded. A domain such as `api.*.node-js-app.com` is treated as a literal hostname and will not match anything.
After enabling and rebuilding, all apps with an `http:80` port mapping will have a corresponding `https:443` added and ssl will be automatically enabled. All http requests will then be redirected to https.
#### Customizing the letsencrypt email per app
The `letsencrypt-email-prod` and `letsencrypt-email-stag` properties can also be set per app, overriding the global value for that app. This is useful when different apps should register their certificates under different contact emails.
The value resolves in two steps: the app's `letsencrypt-server` selects which server (`prod` or `staging`) is used, and the matching `letsencrypt-email-<server>` property is then resolved as the app-level value, falling back to the global value. Because the two emails are per-server, an app-level `letsencrypt-email-stag` only takes effect once the app's `letsencrypt-server` is set to `staging`.
When an app sets its own email for the selected server, Dokku renders a namespaced cert-manager `Issuer` into the app's own release using that email instead of pointing the app at the shared global `ClusterIssuer`. Apps without an app-level email continue to use the shared `ClusterIssuer` with the global email.
The default value may be set by passing an empty value for the option, which falls the app back to the global value:
The letsencrypt integration is set to the production letsencrypt server by default. This can be changed on an app-level by setting the `letsencrypt-server` property with the `scheduler-k3s:set` command
@@ -295,7 +437,7 @@ The default value may be set by passing an empty value for the option.
Letsencrypt can be completely disabled for a given app by setting the `letsencrypt-server` to `false`
Automatic certificate issuance can be completely disabled for a given app by setting the `letsencrypt-server` to `false`. This is the single off switch for the app, and also disables a [manually managed issuer](#using-a-manually-managed-cert-manager-issuer).
Values are validated when the property is set, so a typo fails immediately rather than breaking the next deploy. The valid values are `prod`, `production`, `stag`, `staging`, and `false`.
#### Using a manually managed cert-manager issuer
Dokku's built-in letsencrypt integration uses an `http01` solver, which cannot issue wildcard certificates and cannot satisfy providers that require `dns01`. For those cases, create a cert-manager `Issuer` or `ClusterIssuer` yourself and point an app at it with the `cert-issuer-name` property.
Dokku does not create, modify, or delete the issuer - it only references it from the app's generated `Certificate`. Any cert-manager issuer works, including non-ACME ones such as `selfSigned`, `ca`, or `vault`.
The issuer kind defaults to `ClusterIssuer`. To reference a namespaced `Issuer`, set the `cert-issuer-kind` property:
> A namespaced `Issuer` must exist in the same namespace as the app, as configured by the `namespace` property. cert-manager cannot reference an `Issuer` across namespaces.
Unlike the letsencrypt integration, no email property is required - setting `cert-issuer-name` is itself what enables https for the app. Certificates are requested for every domain attached to the app.
Both properties can also be set globally, which enables https for every app that has domains, no imported certificate, and no `letsencrypt-server false`:
Certificate sources are resolved in the following order:
1. A certificate imported via the `certs` plugin.
2.`letsencrypt-server` set to `false`, which disables issuance entirely.
3.`cert-issuer-name`, resolved app-first and then globally.
4. The built-in letsencrypt integration.
Because an empty app-level property falls back to the global value, an app cannot return to the built-in letsencrypt integration by unsetting `cert-issuer-name` while a global value is configured. Set the app's `cert-issuer-name` to the reserved value `false` instead:
If the named issuer does not exist in the cluster, Dokku emits a warning before the build starts. The warning never blocks a deploy, since the issuer may be managed independently and applied later. When a certificate fails to issue, inspect it directly:
A `dns01` issuer can issue wildcard certificates. Add the wildcard as a domain on the app, and it will be included in the generated `Certificate`:
```shell
dokku domains:add node-js-app '*.node-js-app.com'
```
See [wildcard domains](#wildcard-domains) for how a wildcard is matched against incoming requests.
#### Using imported SSL certificates
SSL certificates imported via the `certs` plugin can be used with the k3s scheduler. When a certificate is imported, it is automatically synced to Kubernetes as a TLS secret and will be used for the app's ingress configuration.
@@ -321,9 +529,9 @@ When a certificate is imported:
- A Kubernetes TLS secret named `tls-<app-name>` is created in the app's namespace
- The ingress configuration is updated to use the imported certificate
- Automatic Let's Encrypt certificate generation is disabled for the app
- Automatic certificate generation is disabled for the app
Imported certificates take precedence over Let's Encrypt certificates. If you have both an imported certificate and Let's Encrypt configured, the imported certificate will be used.
Imported certificates take precedence over both Let's Encrypt and a [manually managed issuer](#using-a-manually-managed-cert-manager-issuer). If you have an imported certificate alongside either of those, the imported certificate will be used.
To remove an imported certificate:
@@ -372,6 +580,8 @@ The following resource types are supported:
-`traefik_ingressroute`
-`traefik_middleware`
Annotation keys may contain `/` (e.g. Kubernetes-style keys such as `prometheus.io/scrape`) and values may span multiple lines; both are preserved verbatim.
A `ps:restart` is required after setting annotations in order to have them apply to running resources.
#### Removing an annotation
@@ -385,6 +595,35 @@ dokku scheduler-k3s:annotations:set node-js-app annotation.key --resource-type d
A `ps:restart` is required after removing annotations in order to remove them from running resources.
#### Displaying annotations
Configured annotations can be inspected with the `scheduler-k3s:annotations:report` command. Without arguments, it iterates every app and prints all annotations. Passing an app name (or `--global`) scopes the report:
dokku scheduler-k3s:annotations:report node-js-app --process-type web --resource-type deployment
```
JSON output emits flat keys of the form `{process_type}.{resource_type}.{annotation_key}`. The literal `--global` process type is rendered as `global` to keep keys free of leading dashes:
Dokku injects certain labels into each created resource by default, but it may be necessary to inject others for tighter integration with third-party tools. The `scheduler-k3s:labels:set` command can be used to perform this task. The command takes an app name and a required `--resource-type` flag.
@@ -413,6 +652,8 @@ The following resource types are supported:
-`traefik_ingressroute`
-`traefik_middleware`
Label keys may contain `/` (e.g. Kubernetes-style keys such as `app.kubernetes.io/part-of`) and values may span multiple lines; both are preserved verbatim.
A `ps:restart` is required after setting labels in order to have them apply to running resources.
##### Displaying an Authentication Resource report
To see a list of authentication resources managed by Dokku, run the `scheduler-k3s:autoscaling-auth:report` command.
To see a list of authentication resources managed by Dokku, run the `scheduler-k3s:autoscaling-auth:report` command. Without arguments, the report iterates every app; passing an app name (or `--global`) scopes the report:
By default, the report will not display configured metadata - making it safe to include in Dokku report output. To include metadata and their values, add the `--include-metadata` flag:
JSON output emits flat keys of the form `{trigger}.{metadata_key}` and includes the actual metadata values so export tools can reconstruct the configured state:
A single configured metadata key can also be queried with a flag of the form `--scheduler-k3s-autoscaling-auth.{trigger}.{metadata_key}`. The returned value is masked in the same way as stdout output; use `--format json` to read the actual value:
Kernel sysctls fall into two categories, and which one a sysctl belongs to determines how it must be set.
The kernel maintains a per-namespace copy of `net.*` (network namespace) as well as `kernel.shm*`, `kernel.msg*`, `kernel.sem`, and `fs.mqueue.*` (IPC namespace). These can be set on a single app's pods. Every other sysctl - including all of `vm.*`, and therefore `vm.max_map_count` - holds a single value shared by the entire machine, so it cannot be scoped to a pod and must be applied to the node itself.
#### Namespaced sysctls
Namespaced sysctls are set with the `docker-options` plugin, and are translated into the pod's `securityContext.sysctls`. A `ps:restart` is required to apply them.
Passing a non-namespaced sysctl this way fails the deploy rather than silently dropping the value, since it provably cannot take effect within a pod. Note this differs from the `docker-local` scheduler, where such an option is passed straight through to `docker run`.
Kubernetes further splits namespaced sysctls into a *safe* list that any pod may set, and everything else. A sysctl outside the safe list - `net.core.somaxconn`, for example - is rejected at pod admission unless the node's kubelet was started with a matching `allowed-unsafe-sysctls` value, which can be supplied at cluster initialization or when joining a node.
Dokku does not enforce the safe list itself, as its membership changes between Kubernetes releases. Only the namespaced/non-namespaced distinction, which is a property of the kernel, is validated.
#### Non-namespaced sysctls
Non-namespaced sysctls are a property of the node, not of any app, and are managed with the `node-sysctls:set` command. Dokku applies them via a privileged DaemonSet, so they reach every node without being told which nodes exist, cover nodes joined later, and are reapplied after a node reboots.
Clearing a sysctl stops Dokku managing it, but does not restore whatever the node had before. The last value written stays in place until that node reboots, which is how `sysctl -w` behaves everywhere else.
Sysctls can also be scoped to a [node profile](#node-profiles) with `--profile`, which applies them only to nodes joined with that profile.
A profile scope inherits everything set globally and overrides it on conflict, so each node is covered by exactly one DaemonSet and no two ever write the same value. Note that the server node created by `scheduler-k3s:initialize` never carries a profile label, so only globally-scoped sysctls reach it.
Use `node-sysctls:report` to see the resolved set for every scope.
Dokku supports integration with [Kustomize](https://kustomize.io/) to further customize the generated helm charts for app deployments. For example, a `config/kustomize/kustomization.yaml` file with the following contents will override the scale for each process deployed to `3`:
@@ -658,21 +995,34 @@ The default value for the `kube-context` is an empty string, and will result in
### Customizing Helm Chart Properties
Dokku includes a number of helm charts by default with settings that are optimized for Dokku. That said, it may be useful to further customize the charts for a given environment. Users can customize which charts are installed by setting properties prefixed with `chart.$CHART_NAME.` with the `--global` flag.
Dokku includes a number of helm charts by default with settings that are optimized for Dokku. That said, it may be useful to further customize the charts for a given environment. Chart overrides are managed via the `scheduler-k3s:charts:set` command, which takes a `<chart-name>.<property>` argument and a value. Chart overrides are always global because helm charts are not managed on a per-app basis.
> Properties follow dot-notation, and are expanded according to Helm's internal logic. See the [Helm documentation](https://helm.sh/docs/helm/helm_install/#helm-install) for `helm install` for further details.
To unset a chart property, omit the value from the `scheduler-k3s:set` call:
Property names may contain `/` (e.g. for Kubernetes-style annotation keys such as `service.annotations.prometheus.io/scrape`) and values may span multiple lines; both are preserved verbatim.
To unset a chart property, omit the value from the `scheduler-k3s:charts:set` call:
Configured chart overrides can be inspected with the `scheduler-k3s:charts:report` command. Without an argument, it lists every chart known to Dokku along with any configured overrides; passing a chart name scopes the report to that chart:
```shell
dokku scheduler-k3s:charts:report
dokku scheduler-k3s:charts:report cert-manager
dokku scheduler-k3s:charts:report --format json
```
> [!NOTE]
> The legacy form `dokku scheduler-k3s:set --global chart.<chart>.<property> <value>` continues to work but is deprecated and will be removed in a future major release. Migrate to `scheduler-k3s:charts:set` instead.
A `scheduler-k3s:ensure-charts` command with the `--force` flag is required after changing any chart properties in order to have them apply. This will install all charts, not just the ones that have changed.
```shell
@@ -702,7 +1052,7 @@ dokku storage:wait demo-data
git push dokku master
```
For a hostPath-backed PV (no StorageClass), pass `<path>` as the second positional argument and omit `--storage-class-name`. The plugin renders both the PV and the PVC into the entry's helm release. The `--reclaim-policy` flag (`Retain` or `Delete`) controls whether the underlying PV survives `storage:destroy`. Annotations and labels on`storage:create` /`storage:set` propagate to both the PVC and the PV so backup tools (Velero, Longhorn snapshots) can find them.
For a hostPath-backed PV (no StorageClass), pass `<path>` as the second positional argument and omit `--storage-class-name`. The plugin renders both the PV and the PVC into the entry's helm release. The `--reclaim-policy` flag (`Retain` or `Delete`) controls whether the underlying PV survives `storage:destroy`. Annotations and labels set via`storage:annotations:set` and`storage:labels:set` propagate to both the PVC and the PV so backup tools (Velero, Longhorn snapshots) can find them.
The legacy `storage:mount <app> <host>:<container>` colon form is rejected on k3s apps; create a named entry instead. See [Persistent Storage](/docs/advanced-usage/persistent-storage.md) for the full command reference.
@@ -730,6 +1080,7 @@ This plugin implements various functionality through `plugn` triggers to integra
-`--cap-add`
-`--cap-drop`
-`--privileged`
-`--sysctl` (namespaced sysctls only, see [Setting kernel sysctls](#setting-kernel-sysctls))
-`cron`
-`enter`
-`deploy`
@@ -742,6 +1093,8 @@ This plugin implements various functionality through `plugn` triggers to integra
- Properties set by the `nginx` plugin will be respected, either by turning them into annotations or creating a custom server/location snippet that the `ingress-nginx` project can use. A `ps:restart` after changing any nginx properties is required in order to have them apply.
- The `nginx:access-logs` and `nginx:error-logs` commands will fetch logs from one running `ingress-nginx` pod.
- The `nginx:show-config` command will retrieve any `server` blocks associated with a domain attached to the app from one running `ingress-nginx` pod.
-`ps:restart`
- Supports targeting a single process type, see [Restarting apps](#restarting-apps)
-`ps:stop`
-`run`
- The `scheduler-post-run` trigger is not always triggered
Please see the [vector logs documentation](/docs/deployment/logs.md#configuring-a-log-sink) for more information on specifying vector sinks.
#### Shipping cron task logs
The global `vector-cron-sink` property is also respected. When set, logs from cron task pods are routed to that sink instead of the sink configured for everything else, matching the behavior described in the [cron task log sink documentation](/docs/deployment/logs.md#configuring-a-cron-task-log-sink).
As with `vector-sink`, only the global property is respected - a per-app `vector-cron-sink` has no effect on the `k3s` scheduler.
Cron events carry the same `dokku_app` and `dokku_cron_id` fields as they do on the `docker-local` scheduler, so sink configuration referencing them is portable between the two.
Two differences are worth noting:
- Templated values must be base64 encoded. Sink values containing `{{ }}` are interpreted by Helm at chart install time rather than by Vector, so the `base64enc:` form documented under [log sink DSN format](/docs/deployment/logs.md#log-sink-dsn-format) is required.
- The `file` sink is not useful here. Vector runs as a DaemonSet agent, so a file path resolves to whichever node the agent is running on rather than to durable shared storage. Use a network sink and reference `dokku_cron_id` as a field instead of as a path component.
### Supported Resource Management Properties
The `k3s` scheduler supports a minimal list of resource _limits_ and _reservations_:
@@ -787,3 +1158,32 @@ If unspecified for any task, the default reservation will be `.1` CPU and `128Mi
> [!NOTE]
> Cron tasks retrieve resource limits based on the computed cron task ID.
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `scheduler-k3s:report`. The JSON keys emitted by `scheduler-k3s:report --format json` are the same names with the leading `--scheduler-k3s-` stripped (e.g. `deploy-timeout`, `global-deploy-timeout`, `computed-deploy-timeout`). Legacy keys with the `scheduler-k3s-` prefix are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `cert-issuer-kind` | app + global | `ClusterIssuer` | `--scheduler-k3s-cert-issuer-kind`, `--scheduler-k3s-global-cert-issuer-kind`, `--scheduler-k3s-computed-cert-issuer-kind` | Kind of the manually managed cert-manager issuer referenced by `cert-issuer-name`, either `Issuer` or `ClusterIssuer` |
| `cert-issuer-name` | app + global | none | `--scheduler-k3s-cert-issuer-name`, `--scheduler-k3s-global-cert-issuer-name`, `--scheduler-k3s-computed-cert-issuer-name` | Name of a manually managed cert-manager issuer to request certificates from, taking precedence over the letsencrypt integration. Set to `false` to opt an app out of a global value |
| `deploy-timeout` | app + global | `300s` | `--scheduler-k3s-deploy-timeout`, `--scheduler-k3s-global-deploy-timeout`, `--scheduler-k3s-computed-deploy-timeout` | Timeout for a single helm install/upgrade cycle |
| `image-pull-secrets` | app + global | none | `--scheduler-k3s-image-pull-secrets`, `--scheduler-k3s-global-image-pull-secrets`, `--scheduler-k3s-computed-image-pull-secrets` | Comma-separated list of Kubernetes secret names used to pull private images |
| `ingress-class` | global only | `nginx` | `--scheduler-k3s-global-ingress-class`, `--scheduler-k3s-computed-ingress-class` | IngressClass name used for app ingresses (e.g. `nginx`, `traefik`) |
| `kube-context` | global only | none | `--scheduler-k3s-global-kube-context`, `--scheduler-k3s-computed-kube-context` | Kube context name used by helm and kubectl invocations |
| `kubeconfig-path` | global only | `/etc/rancher/k3s/k3s.yaml` | `--scheduler-k3s-global-kubeconfig-path`, `--scheduler-k3s-computed-kubeconfig-path` | Filesystem path to the kubeconfig used to talk to the cluster |
| `kustomize-root-path` | app + global | `config/kustomize` | `--scheduler-k3s-kustomize-root-path`, `--scheduler-k3s-global-kustomize-root-path`, `--scheduler-k3s-computed-kustomize-root-path` | Path within the app to a kustomize root applied after the helm install |
| `letsencrypt-email-prod` | app + global | none | `--scheduler-k3s-letsencrypt-email-prod`, `--scheduler-k3s-global-letsencrypt-email-prod`, `--scheduler-k3s-computed-letsencrypt-email-prod` | Contact email for production certificates. App-level values render a per-app namespaced Issuer; otherwise the shared production ClusterIssuer is used |
| `letsencrypt-email-stag` | app + global | none | `--scheduler-k3s-letsencrypt-email-stag`, `--scheduler-k3s-global-letsencrypt-email-stag`, `--scheduler-k3s-computed-letsencrypt-email-stag` | Contact email for staging certificates. App-level values render a per-app namespaced Issuer; otherwise the shared staging ClusterIssuer is used |
| `letsencrypt-server` | app + global | `prod` | `--scheduler-k3s-letsencrypt-server`, `--scheduler-k3s-global-letsencrypt-server`, `--scheduler-k3s-computed-letsencrypt-server` | ACME directory (`prod` or `staging`) used for app certificates, or `false` to disable all automatic certificate issuance |
| `namespace` | app + global | `default` | `--scheduler-k3s-namespace`, `--scheduler-k3s-global-namespace`, `--scheduler-k3s-computed-namespace` | Kubernetes namespace into which the app's resources are installed |
| `network-interface` | global only | `eth0` | `--scheduler-k3s-global-network-interface`, `--scheduler-k3s-computed-network-interface` | Host network interface used by k3s |
| `node-sysctls-image` | global only | `busybox:1.36` | `--scheduler-k3s-global-node-sysctls-image` | Image used to apply node-level sysctls, override for air-gapped clusters |
| `node-sysctls-pause-image` | global only | `registry.k8s.io/pause:3.9` | `--scheduler-k3s-global-node-sysctls-pause-image` | Image keeping the node sysctls daemonset pods running |
| `rollback-on-failure` | app + global | `false` | `--scheduler-k3s-rollback-on-failure`, `--scheduler-k3s-global-rollback-on-failure`, `--scheduler-k3s-computed-rollback-on-failure` | When `true`, helm rolls back the release if a deploy fails |
| `token` | global only | none | `--scheduler-k3s-global-token` (masked as `*******` in default stdout output; the raw value is returned when queried via `--format json` or when this flag is requested explicitly) | Cluster join token used by `scheduler-k3s:cluster-add` |
| `chart.<chart-name>.<property>` | global only | none | `--scheduler-k3s-global-chart.<chart-name>.<property>` (dynamic per chart/property) | Override a value injected into the helm chart named `<chart-name>` (one row per chart/property pair). Manage these via the dedicated `scheduler-k3s:charts:set` / `scheduler-k3s:charts:report` commands; the `scheduler-k3s:set`/`scheduler-k3s:report` form is deprecated. |
The `selected` and `global-selected` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-selected` key holds the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default of `docker-local`.
@@ -143,3 +147,15 @@ Schedulers may decide to omit some functionality here, or use plugin triggers to
Schedulers can use any tools available on the system to build the docker image, and may even be used to interact with off-server systems. The only current requirement is that the scheduler must have access to the image built in the build phase. If this is not the case, the registry plugin can be used to push the image to a registry that the scheduler software can access.
Deployment tasks are currently executed directly on the primary Dokku server.
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `scheduler:report`. The JSON keys emitted by `scheduler:report --format json` are the same names with the leading `--scheduler-` stripped (e.g. `selected`, `global-selected`, `computed-selected`). Legacy keys with the `scheduler-` prefix are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `selected` | app + global | `docker-local` | `--scheduler-selected`, `--scheduler-global-selected`, `--scheduler-computed-selected` | Scheduler plugin used to deploy this app (`docker-local`, `k3s`, etc.) |
| `shell` | app + global | none | `--scheduler-shell`, `--scheduler-global-shell`, `--scheduler-computed-shell` | Shell used by `enter`/`run` commands when entering a container |
The `wait-to-retire` and `global-wait-to-retire` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-wait-to-retire` key holds the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default of `60`.
| `wait-to-retire` | app + global | `60` | `--checks-wait-to-retire`, `--checks-global-wait-to-retire`, `--checks-computed-wait-to-retire` | Seconds to wait between deploying the new container and stopping the old one |
- Description: Removes the SSL cert/key pair from an app and fires the `post-certs-remove` and `post-domains-update` triggers. Fails if no app-specific SSL endpoint is defined.
- Invoked by:
- Arguments: `$APP`
- Example:
```shell
#!/usr/bin/env bash
# Removes the SSL endpoint for an app
set -eo pipefail;[[$DOKKU_TRACE]]&&set -x
APP="$1"
dokku certs:remove "$APP"
```
### `certs-set`
- Description: Installs an SSL cert/key pair onto an app and fires the `post-certs-update` and `post-domains-update` triggers. `$CRT_FILE` and `$KEY_FILE` must be paths to readable PEM-encoded files.
- Invoked by:
- Arguments: `$APP $CRT_FILE $KEY_FILE`
- Example:
```shell
#!/usr/bin/env bash
# Installs a cert/key pair for an app
set -eo pipefail;[[$DOKKU_TRACE]]&&set -x
APP="$1";CRT_FILE="$2";KEY_FILE="$3"
dokku certs:add "$APP""$CRT_FILE""$KEY_FILE"
```
### `check-deploy`
- Description: Allows you to run checks on a deploy before Dokku allows the container to handle requests.
@@ -545,6 +581,21 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `config-migrate-env`
- Description: Drains the pre-0.38 `$DOKKU_ROOT/ENV` and `$DOKKU_ROOT/<app>/ENV` files into the config property path, removing each file once it has been drained. A file found at the old path after its migration has been recorded is never imported: the config path holds every change made since, so the file is removed when it agrees with the current config and otherwise moved aside to `ENV.migrated`. Idempotent, and safe to call from an install trigger that runs before the config plugin's own.
- Invoked by: `common` when migrating deprecated config vars to plugin properties, checks plugin
- Arguments: none
- Example:
```shell
#!/usr/bin/env bash
set -eo pipefail;[[$DOKKU_TRACE]]&&set -x
plugn trigger config-migrate-env
```
### `config-set`
- Description: Sets one or more config values for an app without restarting (when --no-restart flag is specified)
@@ -1304,6 +1355,8 @@ esac
The default templates are viewable here: [plugins/nginx-vhosts/templates/](https://github.com/dokku/dokku/tree/master/plugins/nginx-vhosts/templates)
Overriding the `validate-config` template is the supported way to make deploy-time pre-validation aware of directives from nginx modules that the host loads via `load_module` in its global config. As implementing this trigger requires a [custom plugin](/docs/development/plugin-creation.md), see [Custom nginx modules](/docs/appendices/file-formats/nginx-conf-sigil.md#custom-nginx-modules) for a complete example.
### `nginx-dokku-template-source`
- Description: Return the path to a `sigil` template that should be used to generate the `dokku.conf` nginx configuration file.
> The scheduler plugin trigger apis are under development and may change
> between minor releases until the 1.0 release.
- Description: Force triggers writing out cron tasks. Arguments are optional.
- Invoked by: `ps:start`, `ps:stop`, `cron:set`
- Description: Force triggers writing out cron tasks. Arguments are optional. The `cron` plugin implements this for host-crontab schedulers (regenerating the whole `dokku` user crontab when the scheduler uses host cron or when no scheduler is given); self-managed schedulers such as `k3s` implement it to update their own cron backend.
> The scheduler plugin trigger apis are under development and may change
> between minor releases until the 1.0 release.
- Description: Allows you to run scheduler commands when an app is deployed
- Description: Allows you to run scheduler commands when an app is deployed. `$PROCESS_TYPE` is empty for a normal deploy and set when a single process type is targeted, as by `dokku ps:restart <app> <process-type>`, in which case only that process type should be redeployed.
- Flags: `--interactive` (stdin is open), `--tty` (stdin is a terminal), `--as-user <uid>` (override `entry.Chown`).
### `scheduler-uses-host-cron`
> [!WARNING]
> The scheduler plugin trigger apis are under development and may change
> between minor releases until the 1.0 release.
- Description: Reports whether the scheduler writes `app.json` cron tasks to the host `dokku` user crontab. Schedulers that use the host crontab (`docker-local`) echo `true`; schedulers that manage their own cron backend (`k3s`, which creates in-cluster CronJobs) echo `false`. The cron plugin reads this to decide which apps to include when regenerating the host crontab; a scheduler that does not implement the trigger is treated as `false`.
@@ -27,7 +27,7 @@ Alternatively, you can use `docker-compose.yml`:
```yaml
services:
dokku:
image:dokku/dokku:0.38.1
image:dokku/dokku:0.38.27
container_name:dokku
network_mode:bridge
ports:
@@ -44,7 +44,38 @@ services:
restart:unless-stopped
```
The above command will start a new docker container that is ready when a message similar to `Runit started as PID 12345` appears.
## Container readiness
The image ships with a Docker `HEALTHCHECK` that flips from `starting` to `healthy` once first-boot bootstrap is complete (skel restored, `plugin-list` plugins installed, core install triggers fired), nginx and sshd are accepting connections, and `dokku ps:restore` has finished. Until those conditions hold, the container is considered unhealthy and dependent services should not yet send traffic.
The endpoint binds to `127.0.0.1:18080` inside the container by design - it is not published to the host and does not interfere with user app vhosts on ports 80/443.
Compose dependents can gate on the healthcheck via `depends_on` with `condition: service_healthy`:
The alternative is to build a custom docker image via a custom Dockerfile. This Dockerfile can run any `plugin:install` command, though the `install` trigger must be skipped via the `--skip-install-trigger`. The version installed at that time will be the one that persists. Below is an example Dockerfile showing this method.
```Dockerfile
FROMdokku/dokku:0.38.1
FROMdokku/dokku:0.38.27
RUN dokku plugin:install https://github.com/dokku/dokku-postgres.git --skip-install-trigger
RUN dokku plugin:install https://github.com/dokku/dokku-redis.git --skip-install-trigger
@@ -84,9 +84,6 @@ Please read the migration guides for each version in between your currently inst
#### Upgrading using `dokku-update`
> [!WARNING]
> The `dokku-update` package currently does not support upgrading to a specific version of Dokku. If this is required by a particular migration guide, use the `apt` method for upgrading.
We provide a helpful binary called `dokku-update`. This is a recommended package that:
- Can be installed separately, so upgrading Dokku will not affect the running of this package.
The `DokkuManaged` field is `true` for networks created by `network:create` and `false` for Docker built-in networks (such as `bridge`, `host`, and `none`) or networks created outside of Dokku (such as compose `*_default` networks). This can be used by automation to determine which networks Dokku is responsible for.
The `network:list` command also takes a `--dokku-managed` flag, which restricts the output to only those networks created by Dokku. It can be combined with the `--format` flag:
```shell
dokku network:list --dokku-managed
```
```
=====> Networks
test-network
```
### Creating a network
> [!IMPORTANT]
@@ -142,10 +155,11 @@ dokku network:info bridge
```
=====> bridge network information
ID: d18df2d21433
Name: bridge
Driver: bridge
Scope: local
ID: d18df2d21433
Name: bridge
Driver: bridge
Scope: local
Dokku managed: false
```
The `network:info` command also takes a `--format` flag, with the valid options including `text` (default) and `json`. The `json` output format can be used for automation purposes:
The per-app and `global-` keys for each property hold the raw values and are empty when nothing has been set. The `computed-` keys hold the effective value, falling back to the global value (where one has been set) and then to the built-in default (`false` for `bind-all-interfaces`, empty otherwise).
> The `Report flags` column lists the CLI argument names accepted by `network:report`. The JSON keys emitted by `network:report --format json` are the same names with the leading `--network-` stripped (e.g. `attach-post-create`, `global-attach-post-create`, `computed-attach-post-create`). Legacy keys with the `network-` prefix (e.g. `network-attach-post-create`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `attach-post-create` | app + global | none | `--network-attach-post-create`, `--network-global-attach-post-create`, `--network-computed-attach-post-create` | Networks attached to a container immediately after creation, before the deploy phase |
| `attach-post-deploy` | app + global | none | `--network-attach-post-deploy`, `--network-global-attach-post-deploy`, `--network-computed-attach-post-deploy` | Networks attached to a container after it passes healthchecks |
| `bind-all-interfaces` | app + global | `false` | `--network-bind-all-interfaces`, `--network-global-bind-all-interfaces`, `--network-computed-bind-all-interfaces` | When `true`, binds containers to `0.0.0.0` instead of the default Docker network address |
| `initial-network` | app + global | none | `--network-initial-network`, `--network-global-initial-network`, `--network-computed-initial-network` | Network attached at container creation time |
| `static-web-listener` | app only | none | `--network-static-web-listener` | Static `host:port` override used in proxy templates when no container is running |
Dokku provides integration with the [Caddy](https://caddyserver.com/) proxy service by utilizing the Docker label-based integration implemented by [Caddy Docker Proxy](https://github.com/lucaslorentz/caddy-docker-proxy).
```
caddy:report [<app>] [<flag>] # Displays a caddy report for one or more apps
The `global-<prop>` keys hold the raw global value and are empty when nothing has been set globally. The `computed-<prop>` keys hold the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default. The bare `tls-internal` key holds the raw per-app value.
| `image` | global only | _parsed from `plugins/caddy-vhosts/Dockerfile`_ | `--caddy-global-image`, `--caddy-computed-image` | Docker image used to run the Caddy container |
| `letsencrypt-email` | global only | none | `--caddy-global-letsencrypt-email`, `--caddy-computed-letsencrypt-email` | Contact email enabling letsencrypt; empty disables https issuance |
| `letsencrypt-server` | global only | `https://acme-v02.api.letsencrypt.org/directory` | `--caddy-global-letsencrypt-server`, `--caddy-computed-letsencrypt-server` | ACME directory used when requesting certificates |
| `log-level` | global only | `ERROR` | `--caddy-global-log-level`, `--caddy-computed-log-level` | Caddy log level |
| `polling-interval` | global only | `5s` | `--caddy-global-polling-interval`, `--caddy-computed-polling-interval` | Frequency at which Caddy polls the Docker API for label changes |
| `tls-internal` | app + global | `false` | `--caddy-tls-internal`, `--caddy-global-tls-internal`, `--caddy-computed-tls-internal` | When `true`, uses Caddy's built-in self-signed TLS instead of letsencrypt |
### Internal properties
The following properties are not managed by `caddy:set` but are recorded internally by the plugin:
| Property | Description | Source |
|---|---|---|
| `proxy-status` | `started`/`stopped` state of the caddy compose project | `cmd-caddy-start`/`cmd-caddy-stop` in `plugins/caddy-vhosts/command-functions` |
The `global-<prop>` keys hold the raw global value and are empty when nothing has been set globally. The `computed-<prop>` keys hold the effective value used at deploy time, falling back to the built-in default when the global value is empty.
| `image` | global only | _parsed from `plugins/haproxy-vhosts/Dockerfile`_ | `--haproxy-global-image`, `--haproxy-computed-image` | Docker image used to run the Haproxy container |
| `letsencrypt-email` | global only | none | `--haproxy-global-letsencrypt-email`, `--haproxy-computed-letsencrypt-email` | Contact email enabling letsencrypt; empty disables https issuance |
| `letsencrypt-server` | global only | `https://acme-v02.api.letsencrypt.org/directory` | `--haproxy-global-letsencrypt-server`, `--haproxy-computed-letsencrypt-server` | ACME directory used when requesting certificates |
| `log-level` | global only | `ERROR` | `--haproxy-global-log-level`, `--haproxy-computed-log-level` | Haproxy log level |
| `refresh-conf` | global only | `10` | `--haproxy-global-refresh-conf`, `--haproxy-computed-refresh-conf` | Seconds between Haproxy polls of the Docker API for label changes |
### Internal properties
The following properties are not managed by `haproxy:set` but are recorded internally by the plugin:
| Property | Description | Source |
|---|---|---|
| `proxy-status` | `started`/`stopped` state of the haproxy compose project | `cmd-haproxy-start`/`cmd-haproxy-stop` in `plugins/haproxy-vhosts/command-functions` |
App-supplied `nginx.conf.sigil` files are pre-validated automatically at the start of every deploy, immediately after the template is extracted from the source tree and before the build phase runs. The template is rendered with sigil and run through `nginx -t` against a minimal wrapper config; if validation fails, the deploy is aborted before any build work begins. To bypass this behavior, set `disable-custom-config` to `true` (see "Disabling custom nginx config" below).
The wrapper config used for validation does not include the top-level `load_module` directives from the global nginx config, so a `nginx.conf.sigil` that relies on a directive from a dynamically loaded module will fail this validation even when `nginx -t` passes against the real server config. See [Custom nginx modules](/docs/appendices/file-formats/nginx-conf-sigil.md#custom-nginx-modules) for how to supply a custom validation wrapper via the `nginx-app-template-source` trigger's `validate-config` template type.
It may also be desired to validate an nginx config outside of the deployment process. To do so, run the `nginx:validate-config` command. With no arguments, this will validate all app nginx configs, one at a time. A minimal wrapper nginx config is generated for each app's nginx config, upon which `nginx -t` will be run.
```shell
@@ -208,6 +210,22 @@ server {
The `00-` prefix forces nginx to load this file before `/etc/nginx/conf.d/dokku.conf`, so its `default_server` markers establish the default for each port before any per-app server blocks are loaded.
On systems running nginx older than 1.19.4 (e.g., Debian Bullseye, which ships nginx 1.18.0), `ssl_reject_handshake` is not available. The postinst detects this and installs an HTTP-only variant instead:
```nginx
server{
listen80default_server;
listen[::]:80default_server;
server_name_;
access_logoff;
return444;
}
```
On these systems, HTTPS requests to unknown hosts are not rejected by the catch-all - nginx falls through to the lexicographically first port-443 server block and presents that block's certificate, the same behavior as before 0.38.0.
#### TLS handshake behavior
The catch-all does not affect TLS handshakes for legitimate apps. nginx selects the matching server block via SNI before completing the handshake; only requests that have no matching app fall through to the catch-all.
@@ -215,8 +233,8 @@ The catch-all does not affect TLS handshakes for legitimate apps. nginx selects
| Request | Result |
| --- | --- |
| HTTPS to a configured app's hostname (with matching SNI) and the app has a cert | Handshake completes with the app's cert. Catch-all not consulted. |
| HTTPS to a configured app's hostname when the app has no cert configured | Handshake rejected by the catch-all. (Previously: nginx fell through to the lexicographically first port-443 server block and presented its cert, producing a confusing cert-mismatch error.) |
| HTTPS to the server's IP with no SNI, or with an SNI matching no app | Handshake rejected. |
| HTTPS to a configured app's hostname when the app has no cert configured | Handshake rejected by the catch-all on nginx 1.19.4+. On older nginx, falls through to the lexicographically first port-443 server block and presents its cert, producing a cert-mismatch error. |
| HTTPS to the server's IP with no SNI, or with an SNI matching no app | Handshake rejected on nginx 1.19.4+. On older nginx, falls through to the first port-443 server block. |
| HTTP to a configured app's hostname | Routed normally to that app. Catch-all not consulted. |
| HTTP to a hostname matching no app | Catch-all `return 444`. |
@@ -604,3 +622,60 @@ See the [ports documentation](/docs/networking/port-management.md) for more info
### Regenerating nginx config
See the [proxy documentation](/docs/networking/proxy-management.md#regenerating-proxy-config) for more information on how to rebuild the nginx proxy configuration for your app.
## Properties
### Settable properties
All nginx-vhosts properties are settable at both the app and global scope.
| `access-log-format` | app + global | none | `--nginx-access-log-format`, `--nginx-global-access-log-format`, `--nginx-computed-access-log-format` | Custom nginx `log_format` directive used for the access log |
| `access-log-path` | app + global | _`{nginx-log-root}/{app}-access.log`_ | `--nginx-access-log-path`, `--nginx-global-access-log-path`, `--nginx-computed-access-log-path` | Path inside the nginx container where access logs are written |
| `bind-address-ipv4` | app + global | none | `--nginx-bind-address-ipv4`, `--nginx-global-bind-address-ipv4`, `--nginx-computed-bind-address-ipv4` | IPv4 address the nginx server block binds to |
| `bind-address-ipv6` | app + global | `::` | `--nginx-bind-address-ipv6`, `--nginx-global-bind-address-ipv6`, `--nginx-computed-bind-address-ipv6` | IPv6 address the nginx server block binds to |
| `client-body-timeout` | app + global | `60s` | `--nginx-client-body-timeout`, `--nginx-global-client-body-timeout`, `--nginx-computed-client-body-timeout` | Time allowed to read the request body from the client |
| `client-header-timeout` | app + global | `60s` | `--nginx-client-header-timeout`, `--nginx-global-client-header-timeout`, `--nginx-computed-client-header-timeout` | Time allowed to read the request header from the client |
| `client-max-body-size` | app + global | `1m` | `--nginx-client-max-body-size`, `--nginx-global-client-max-body-size`, `--nginx-computed-client-max-body-size` | Maximum allowed request body size |
| `disable-custom-config` | app + global | `false` | `--nginx-disable-custom-config`, `--nginx-global-disable-custom-config`, `--nginx-computed-disable-custom-config` | When `true`, ignores app-supplied `nginx.conf.d/*.conf` snippets |
| `error-log-path` | app + global | _`{nginx-log-root}/{app}-error.log`_ | `--nginx-error-log-path`, `--nginx-global-error-log-path`, `--nginx-computed-error-log-path` | Path inside the nginx container where error logs are written |
| `hsts` | app + global | `true` | `--nginx-hsts`, `--nginx-global-hsts`, `--nginx-computed-hsts` | When `true`, emits a `Strict-Transport-Security` header on HTTPS responses |
| `hsts-include-subdomains` | app + global | `true` | `--nginx-hsts-include-subdomains`, `--nginx-global-hsts-include-subdomains`, `--nginx-computed-hsts-include-subdomains` | Adds the `includeSubDomains` directive to the HSTS header |
| `hsts-max-age` | app + global | `15724800` | `--nginx-hsts-max-age`, `--nginx-global-hsts-max-age`, `--nginx-computed-hsts-max-age` | `max-age` value (seconds) in the HSTS header |
| `hsts-preload` | app + global | `false` | `--nginx-hsts-preload`, `--nginx-global-hsts-preload`, `--nginx-computed-hsts-preload` | Adds the `preload` directive to the HSTS header |
| `keepalive-timeout` | app + global | `75s` | `--nginx-keepalive-timeout`, `--nginx-global-keepalive-timeout`, `--nginx-computed-keepalive-timeout` | Time an idle keep-alive connection stays open |
| `lingering-timeout` | app + global | `5s` | `--nginx-lingering-timeout`, `--nginx-global-lingering-timeout`, `--nginx-computed-lingering-timeout` | Time nginx waits for more client data when closing a connection |
| `nginx-conf-sigil-path` | app + global | `nginx.conf.sigil` | `--nginx-nginx-conf-sigil-path`, `--nginx-global-nginx-conf-sigil-path`, `--nginx-computed-nginx-conf-sigil-path` | Path within the app to a custom `nginx.conf.sigil` template |
| `nginx-service-command` | app + global | none | `--nginx-nginx-service-command`, `--nginx-global-nginx-service-command`, `--nginx-computed-nginx-service-command` | Override command used by `nginx:start`/`nginx:stop`/`nginx:reload` |
| `proxy-buffer-size` | app + global | _system pagesize_ | `--nginx-proxy-buffer-size`, `--nginx-global-proxy-buffer-size`, `--nginx-computed-proxy-buffer-size` | Buffer size for reading the first part of the upstream response |
| `proxy-buffers` | app + global | _`8 {pagesize}`_ | `--nginx-proxy-buffers`, `--nginx-global-proxy-buffers`, `--nginx-computed-proxy-buffers` | Number and size of buffers used for an upstream response |
| `proxy-busy-buffers-size` | app + global | _`2 * pagesize`_ | `--nginx-proxy-busy-buffers-size`, `--nginx-global-proxy-busy-buffers-size`, `--nginx-computed-proxy-busy-buffers-size` | Maximum buffer size that can be busy sending a response to the client |
| `proxy-connect-timeout` | app + global | `60s` | `--nginx-proxy-connect-timeout`, `--nginx-global-proxy-connect-timeout`, `--nginx-computed-proxy-connect-timeout` | Time to establish a connection to the upstream |
| `proxy-keepalive` | app + global | none | `--nginx-proxy-keepalive`, `--nginx-global-proxy-keepalive`, `--nginx-computed-proxy-keepalive` | Number of idle keep-alive connections to upstream servers held open per worker |
| `proxy-read-timeout` | app + global | `60s` | `--nginx-proxy-read-timeout`, `--nginx-global-proxy-read-timeout`, `--nginx-computed-proxy-read-timeout` | Time to read a response from the upstream |
| `proxy-send-timeout` | app + global | `60s` | `--nginx-proxy-send-timeout`, `--nginx-global-proxy-send-timeout`, `--nginx-computed-proxy-send-timeout` | Time to transmit a request to the upstream |
| `send-timeout` | app + global | `60s` | `--nginx-send-timeout`, `--nginx-global-send-timeout`, `--nginx-computed-send-timeout` | Time between two successive write operations to the client |
| `underscore-in-headers` | app + global | `off` | `--nginx-underscore-in-headers`, `--nginx-global-underscore-in-headers`, `--nginx-computed-underscore-in-headers` | Whether to allow underscores in client request header field names |
| `x-forwarded-for-value` | app + global | `$remote_addr` | `--nginx-x-forwarded-for-value`, `--nginx-global-x-forwarded-for-value`, `--nginx-computed-x-forwarded-for-value` | Value used for the `X-Forwarded-For` header |
| `x-forwarded-port-value` | app + global | `$server_port` | `--nginx-x-forwarded-port-value`, `--nginx-global-x-forwarded-port-value`, `--nginx-computed-x-forwarded-port-value` | Value used for the `X-Forwarded-Port` header |
| `x-forwarded-proto-value` | app + global | `$scheme` | `--nginx-x-forwarded-proto-value`, `--nginx-global-x-forwarded-proto-value`, `--nginx-computed-x-forwarded-proto-value` | Value used for the `X-Forwarded-Proto` header |
| `x-forwarded-ssl` | app + global | none | `--nginx-x-forwarded-ssl`, `--nginx-global-x-forwarded-ssl`, `--nginx-computed-x-forwarded-ssl` | Value used for the `X-Forwarded-Ssl` header (e.g. `on`/`off`) |
### Read-only flags
The following flag surfaces in `nginx:report` but is not managed by `nginx:set`:
| Flag | Description |
|---|---|
| `--nginx-last-visited-at` | UNIX timestamp of the last request served by nginx for the app |
### Internal properties
The following properties are recorded internally by the plugin and are not exposed via `nginx:report`:
| Property | Description | Source |
|---|---|---|
| `proxy-status` | `started`/`stopped` state of the nginx system service | `cmd-nginx-start`/`cmd-nginx-stop` |
| `nginx-conf-sigil-migrated` | Migration sentinel set the first time a v1 template is rewritten to the current schema | `plugins/nginx-vhosts/install` writes `"true"` once the install-time migration runs |
@@ -124,6 +124,8 @@ The following folders within an app repository may have `*.conf` files that will
-`openresty/http-includes/`: Injected in the `server` block serving http(s) requests for the app.
-`openresty/http-location-includes/`: Injected in the `location` block that proxies to the app in the app's respective `server` block.
Custom snippets filenames may only include alphanumeric, underscore, and dot characters. For security reasons, filenames that contain other characters will be ignored.
### Label Management
The OpenResty plugin allows you to add custom container labels to apps. These labels are injected into containers during deployment and can be used to configure OpenResty behavior beyond what the plugin provides by default.
Five properties (`image`, `log-level`, `letsencrypt-email`, `letsencrypt-server`, `allowed-letsencrypt-domains-func-base64`) are global only. The rest may be set per-app or globally with `--global`; a global value applies to any app that has no per-app value, otherwise the built-in default is used.
Global-only properties expose two report flags: `--openresty-global-<property>` returns the raw stored value (empty when the property has never been set), while `--openresty-computed-<property>` returns the effective value (the global value if set, otherwise the built-in default).
App-or-global properties expose three report flags: `--openresty-<property>` returns the raw per-app value (empty when unset), `--openresty-global-<property>` returns the raw global value (empty when unset), and `--openresty-computed-<property>` returns the effective value, resolving the per-app value first, then the global value, then the built-in default.
| `access-log-format` | app or global | none | `--openresty-access-log-format`, `--openresty-global-access-log-format`, `--openresty-computed-access-log-format` | Custom nginx `log_format` directive used for the access log |
| `access-log-path` | app or global | _`/var/log/nginx/{app}-access.log`_ | `--openresty-access-log-path`, `--openresty-global-access-log-path`, `--openresty-computed-access-log-path` | Path inside the openresty container where access logs are written |
| `allowed-letsencrypt-domains-func-base64` | global only | _allow-all stub_ | `--openresty-global-allowed-letsencrypt-domains-func-base64`, `--openresty-computed-allowed-letsencrypt-domains-func-base64` | Base64-encoded Lua function deciding which domains may request a letsencrypt certificate |
| `bind-address-ipv4` | app or global | none | `--openresty-bind-address-ipv4`, `--openresty-global-bind-address-ipv4`, `--openresty-computed-bind-address-ipv4` | IPv4 address the openresty server block binds to |
| `bind-address-ipv6` | app or global | `::` | `--openresty-bind-address-ipv6`, `--openresty-global-bind-address-ipv6`, `--openresty-computed-bind-address-ipv6` | IPv6 address the openresty server block binds to |
| `client-body-timeout` | app or global | `60s` | `--openresty-client-body-timeout`, `--openresty-global-client-body-timeout`, `--openresty-computed-client-body-timeout` | Time allowed to read the request body from the client |
| `client-header-timeout` | app or global | `60s` | `--openresty-client-header-timeout`, `--openresty-global-client-header-timeout`, `--openresty-computed-client-header-timeout` | Time allowed to read the request header from the client |
| `client-max-body-size` | app or global | `1m` | `--openresty-client-max-body-size`, `--openresty-global-client-max-body-size`, `--openresty-computed-client-max-body-size` | Maximum allowed request body size |
| `error-log-path` | app or global | _`/var/log/nginx/{app}-error.log`_ | `--openresty-error-log-path`, `--openresty-global-error-log-path`, `--openresty-computed-error-log-path` | Path inside the openresty container where error logs are written |
| `hsts` | app or global | `true` | `--openresty-hsts`, `--openresty-global-hsts`, `--openresty-computed-hsts` | When `true`, emits a `Strict-Transport-Security` header on HTTPS responses |
| `hsts-include-subdomains` | app or global | `true` | `--openresty-hsts-include-subdomains`, `--openresty-global-hsts-include-subdomains`, `--openresty-computed-hsts-include-subdomains` | Adds the `includeSubDomains` directive to the HSTS header |
| `hsts-max-age` | app or global | `15724800` | `--openresty-hsts-max-age`, `--openresty-global-hsts-max-age`, `--openresty-computed-hsts-max-age` | `max-age` value (seconds) in the HSTS header |
| `hsts-preload` | app or global | `false` | `--openresty-hsts-preload`, `--openresty-global-hsts-preload`, `--openresty-computed-hsts-preload` | Adds the `preload` directive to the HSTS header |
| `image` | global only | _parsed from `plugins/openresty-vhosts/Dockerfile`_ | `--openresty-global-image`, `--openresty-computed-image` | Docker image used to run the openresty container |
| `keepalive-timeout` | app or global | `75s` | `--openresty-keepalive-timeout`, `--openresty-global-keepalive-timeout`, `--openresty-computed-keepalive-timeout` | Time an idle keep-alive connection stays open |
| `letsencrypt-email` | global only | none | `--openresty-global-letsencrypt-email`, `--openresty-computed-letsencrypt-email` | Contact email enabling letsencrypt; empty disables https issuance |
| `letsencrypt-server` | global only | `https://acme-v02.api.letsencrypt.org/directory` | `--openresty-global-letsencrypt-server`, `--openresty-computed-letsencrypt-server` | ACME directory used when requesting certificates |
| `lingering-timeout` | app or global | `5s` | `--openresty-lingering-timeout`, `--openresty-global-lingering-timeout`, `--openresty-computed-lingering-timeout` | Time openresty waits for more client data when closing a connection |
| `log-level` | global only | `ERROR` | `--openresty-global-log-level`, `--openresty-computed-log-level` | Openresty log level |
| `proxy-buffer-size` | app or global | _system pagesize_ | `--openresty-proxy-buffer-size`, `--openresty-global-proxy-buffer-size`, `--openresty-computed-proxy-buffer-size` | Buffer size for reading the first part of the upstream response |
| `proxy-buffering` | app or global | `on` | `--openresty-proxy-buffering`, `--openresty-global-proxy-buffering`, `--openresty-computed-proxy-buffering` | Whether openresty buffers upstream responses (`on` or `off`) |
| `proxy-buffers` | app or global | _`8 {pagesize}`_ | `--openresty-proxy-buffers`, `--openresty-global-proxy-buffers`, `--openresty-computed-proxy-buffers` | Number and size of buffers used for an upstream response |
| `proxy-busy-buffers-size` | app or global | _`2 * pagesize`_ | `--openresty-proxy-busy-buffers-size`, `--openresty-global-proxy-busy-buffers-size`, `--openresty-computed-proxy-busy-buffers-size` | Maximum buffer size that can be busy sending a response to the client |
| `proxy-connect-timeout` | app or global | `60s` | `--openresty-proxy-connect-timeout`, `--openresty-global-proxy-connect-timeout`, `--openresty-computed-proxy-connect-timeout` | Time to establish a connection to the upstream |
| `proxy-read-timeout` | app or global | `60s` | `--openresty-proxy-read-timeout`, `--openresty-global-proxy-read-timeout`, `--openresty-computed-proxy-read-timeout` | Time to read a response from the upstream |
| `proxy-send-timeout` | app or global | `60s` | `--openresty-proxy-send-timeout`, `--openresty-global-proxy-send-timeout`, `--openresty-computed-proxy-send-timeout` | Time to transmit a request to the upstream |
| `send-timeout` | app or global | `60s` | `--openresty-send-timeout`, `--openresty-global-send-timeout`, `--openresty-computed-send-timeout` | Time between two successive write operations to the client |
| `underscore-in-headers` | app or global | `off` | `--openresty-underscore-in-headers`, `--openresty-global-underscore-in-headers`, `--openresty-computed-underscore-in-headers` | Whether to allow underscores in client request header field names |
| `x-forwarded-for-value` | app or global | `$remote_addr` | `--openresty-x-forwarded-for-value`, `--openresty-global-x-forwarded-for-value`, `--openresty-computed-x-forwarded-for-value` | Value used for the `X-Forwarded-For` header |
| `x-forwarded-port-value` | app or global | `$server_port` | `--openresty-x-forwarded-port-value`, `--openresty-global-x-forwarded-port-value`, `--openresty-computed-x-forwarded-port-value` | Value used for the `X-Forwarded-Port` header |
| `x-forwarded-proto-value` | app or global | `$scheme` | `--openresty-x-forwarded-proto-value`, `--openresty-global-x-forwarded-proto-value`, `--openresty-computed-x-forwarded-proto-value` | Value used for the `X-Forwarded-Proto` header |
| `x-forwarded-ssl` | app or global | none | `--openresty-x-forwarded-ssl`, `--openresty-global-x-forwarded-ssl`, `--openresty-computed-x-forwarded-ssl` | Value used for the `X-Forwarded-Ssl` header (e.g. `on`/`off`) |
The `dns-provider-` prefix will be stripped and the variable name will be uppercased when passed to the Traefik container. For example, `dns-provider-cf_api_email` becomes `CF_API_EMAIL`.
Each configured variable is surfaced by `traefik:report` as `--traefik-global-dns-provider-<env_var>`. As these values are provider credentials, they are masked as `*******` in the default report output, including the aggregate `dokku report`. The raw value is returned when the flag is requested explicitly or when the report is rendered as json:
After configuring, the Traefik container will need to be restarted and apps will need to be rebuilt.
Refer to the [Traefik DNS Challenge documentation](https://doc.traefik.io/traefik/https/acme/#dnschallenge) for the list of supported DNS providers and their required environment variables.
The `global-<prop>` keys hold the raw global value and are empty when nothing has been set globally. The `computed-<prop>` keys hold the effective value used at deploy time, falling back to the built-in default when the global value is empty.
| `api-enabled` | global only | `false` | `--traefik-global-api-enabled`, `--traefik-computed-api-enabled` | When `true`, enables the Traefik HTTP API |
| `api-entry-point` | global only | none | `--traefik-global-api-entry-point`, `--traefik-computed-api-entry-point` | Name of the entry point used by the Traefik API |
| `api-entry-point-address` | global only | none | `--traefik-global-api-entry-point-address`, `--traefik-computed-api-entry-point-address` | Address (`host:port`) the Traefik API listens on |
| `api-vhost` | global only | `traefik.dokku.me` | `--traefik-global-api-vhost`, `--traefik-computed-api-vhost` | Virtual host that routes to the Traefik API |
| `basic-auth-password` | global only | none | `--traefik-global-basic-auth-password`, `--traefik-computed-basic-auth-password` (masked as `*******` in the default stdout report; the raw value is returned when queried via `--format json` or when one of these flags is requested explicitly) | Password for basic auth in front of the API/dashboard |
| `basic-auth-username` | global only | none | `--traefik-global-basic-auth-username`, `--traefik-computed-basic-auth-username` | Username for basic auth in front of the API/dashboard |
| `challenge-mode` | global only | `tls` | `--traefik-global-challenge-mode`, `--traefik-computed-challenge-mode` | ACME challenge method used by Traefik (`tls`, `http`, or `dns`) |
| `dashboard-enabled` | global only | `false` | `--traefik-global-dashboard-enabled`, `--traefik-computed-dashboard-enabled` | When `true`, enables the Traefik dashboard |
| `dns-provider` | global only | none | `--traefik-global-dns-provider`, `--traefik-computed-dns-provider` | Lego DNS provider name used when `challenge-mode` is `dns` |
| `dns-provider-<ENV_VAR>` | global only | none | `--traefik-global-dns-provider-<env_var>` (masked as `*******` in the default stdout report; the raw value is returned when queried via `--format json` or when this flag is requested explicitly) | Per-provider environment variables passed to the Traefik container; `<ENV_VAR>` is the upstream variable name (e.g. `dns-provider-cloudflare-api-token`) |
| `http-entry-point` | global only | `http` | `--traefik-global-http-entry-point`, `--traefik-computed-http-entry-point` | Entry point name handling plaintext HTTP traffic |
| `https-entry-point` | global only | `https` | `--traefik-global-https-entry-point`, `--traefik-computed-https-entry-point` | Entry point name handling TLS-terminated HTTPS traffic |
| `image` | global only | _parsed from `plugins/traefik-vhosts/Dockerfile`_ | `--traefik-global-image`, `--traefik-computed-image` | Docker image used to run the Traefik container |
| `letsencrypt-email` | global only | none | `--traefik-global-letsencrypt-email`, `--traefik-computed-letsencrypt-email` | Contact email enabling letsencrypt; empty disables https issuance |
| `letsencrypt-server` | global only | `https://acme-v02.api.letsencrypt.org/directory` | `--traefik-global-letsencrypt-server`, `--traefik-computed-letsencrypt-server` | ACME directory used when requesting certificates |
| `log-level` | global only | `ERROR` | `--traefik-global-log-level`, `--traefik-computed-log-level` | Traefik log level |
### Internal properties
The following properties are not managed by `traefik:set` but are recorded internally by the plugin:
| Property | Description | Source |
|---|---|---|
| `proxy-status` | `started`/`stopped` state of the traefik compose project | `cmd-traefik-start`/`cmd-traefik-stop` in `plugins/traefik-vhosts/command-functions` |
The `type` and `global-type` keys hold the raw per-app and global value respectively, and are empty when nothing has been set. The `computed-type` key holds the effective value used at deploy time, falling back to the global value (where one has been set) and then to the built-in default of `nginx`.
@@ -226,3 +236,25 @@ Proxy implementations may decide to omit some functionality here, or use plugin
Individual proxy implementations _may_ trigger app rebuilds, depending on how proxy metadata is exposed for the proxy implementation.
Finally, proxy implementations _may_ install extra software needed for the proxy itself in whatever manner deemed fit. Proxy software can run on the host itself or within a running Docker container with either exposed ports or host networking.
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `proxy:report`. The JSON keys emitted by `proxy:report --format json` are the same names with the leading `--proxy-` stripped (e.g. `type`, `global-type`, `computed-type`). Legacy keys with the `proxy-` prefix (e.g. `proxy-type`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `disabled` | app only | `false` | `--proxy-disabled`, `--proxy-computed-disabled` (also exposed inverted as `--proxy-enabled`) | When `true`, disables proxy integration for this app (`proxy:enable`/`proxy:disable` write this) |
| `proxy-port` | app + global | none | `--proxy-proxy-port`, `--proxy-global-proxy-port`, `--proxy-computed-proxy-port` | Override port used for the HTTP listener in the generated proxy config |
| `proxy-ssl-port` | app + global | none | `--proxy-proxy-ssl-port`, `--proxy-global-proxy-ssl-port`, `--proxy-computed-proxy-ssl-port` | Override port used for the HTTPS listener in the generated proxy config |
| `type` | app + global | `nginx` | `--proxy-type`, `--proxy-global-type`, `--proxy-computed-type` | Proxy implementation handling traffic for the app (`nginx`, `caddy`, `haproxy`, `traefik`, `openresty`, or a custom plugin) |
### Read-only flags
The following flags surface in `proxy:report` but are not managed by `proxy:set`:
| Flag | Description |
|---|---|
| `--proxy-enabled` | `true` when the app's `disabled` property is not `true` |
ps:inspect <app> # Displays a sanitized version of docker inspect for an app
ps:rebuild [--parallel count] [--all|<app>] # Rebuilds an app from source
ps:report [<app>] [<flag>] # Displays a process report for one or more apps
ps:restart [--parallel count] [--all|<app>] [<process-name>] # Restart an app
ps:restore [<app>] # Start previously running apps e.g. after reboot
ps:scale [--skip-deploy] <app> <proc>=<count> [<proc>=<count>...] # Get/Set how many instances of a given process to run
ps:set <app> <key> <value> # Set or clear a ps property for an app
ps:start [--parallel count] [--all|<app>] # Start an app
ps:stop [--parallel count] [--all|<app>] # Stop an app
ps:inspect <app> # Displays a sanitized version of docker inspect for an app
ps:rebuild [--parallel count] [--all|<app>] # Rebuilds an app from source
ps:report [<app>] [<flag>] # Displays a process report for one or more apps
ps:restart [--parallel count] [--all|<app>] [<process-name>] # Restart an app
ps:restore [<app>] # Start previously running apps e.g. after reboot
ps:scale [--skip-deploy] [--format stdout|json] <app> [<proc>=<count>...] # Get/Set how many instances of a given process to run
ps:set <app> <key> <value> # Set or clear a ps property for an app
ps:start [--parallel count] [--all|<app>] # Start an app
ps:stop [--parallel count] [--all|<app>] # Stop an app
```
## Usage
@@ -107,6 +107,18 @@ proctype: qty
web: 1
```
The formation can also be retrieved as JSON by using the `--format json` flag, which is useful for programmatic consumption. Each entry maps a process type to its desired quantity:
When no scale has been set for the app, the JSON output is an empty array (`[]`).
### Changing process management settings
The `ps` plugin provides a number of settings that can be used to managed deployments on a per-app basis. The following table outlines ones not covered elsewhere:
The default policy (`on-failure:10`) may be restored by passing an empty value:
```shell
dokku ps:set node-js-app restart-policy
```
A global default may also be set, and is used by any app that does not have an app-specific restart policy:
```shell
dokku ps:set --global restart-policy always
```
The effective policy applied to an app's containers is resolved as the app-specific value, then the global value, then the built-in `on-failure:10` default. This computed value can be inspected via the `--ps-computed-restart-policy` report flag.
Restart policies have no bearing on server reboot, and Dokku will always attempt to restart your apps at that point unless they were manually stopped.
Dokku also runs `dokku-event-listener` in the background via the system's init service. This monitors container state, performing the following actions:
@@ -391,8 +417,10 @@ dokku ps:report
Processes: 0
Ps can scale: true
Ps computed procfile path: Procfile2
Ps computed restart policy: on-failure:10
Ps global procfile path: Procfile
Ps restart policy: on-failure:10
Ps global restart policy:
Ps restart policy:
Ps procfile path: Procfile2
Restore: true
Running: false
@@ -401,8 +429,10 @@ dokku ps:report
Processes: 0
Ps can scale: true
Ps computed procfile path: Procfile
Ps computed restart policy: on-failure:10
Ps global procfile path: Procfile
Ps restart policy: on-failure:10
Ps global restart policy:
Ps restart policy:
Ps procfile path:
Restore: true
Running: false
@@ -411,8 +441,10 @@ dokku ps:report
Processes: 0
Ps can scale: true
Ps computed procfile path: Procfile
Ps computed restart policy: on-failure:10
Ps global procfile path: Procfile
Ps restart policy: on-failure:10
Ps global restart policy:
Ps restart policy:
Ps procfile path:
Restore: true
Running: false
@@ -429,7 +461,9 @@ dokku ps:report node-js-app
Deployed: false
Processes: 0
Ps can scale: true
Ps restart policy: on-failure:10
Ps computed restart policy: on-failure:10
Ps global restart policy:
Ps restart policy:
Restore: true
Running: false
```
@@ -463,3 +497,32 @@ The default value (`true`) may be restored by passing an empty value:
```shell
dokku ps:set node-js-app restore
```
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `ps:report`. The JSON keys emitted by `ps:report --format json` are the same names with the leading `--ps-` stripped (e.g. `procfile-path`, `global-procfile-path`, `computed-procfile-path`). Legacy keys with the `ps-` prefix (e.g. `ps-procfile-path`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `dockerfile-start-cmd` | app only | none | `--ps-dockerfile-start-cmd`, `--ps-computed-dockerfile-start-cmd` | Override `CMD` for Dockerfile-based apps |
| `procfile-path` | app + global | `Procfile` | `--ps-procfile-path`, `--ps-global-procfile-path`, `--ps-computed-procfile-path` | Path to the app's Procfile, relative to the build root |
| `restore` | app only | `true` | `--restore` | When `true`, the app is restarted automatically by `ps:retire` after a host reboot |
| `skip-deploy` | app + global | `false` | `--ps-skip-deploy`, `--ps-global-skip-deploy`, `--ps-computed-skip-deploy` | When `true`, skips the deploy phase after a successful build |
| `start-cmd` | app only | none | `--ps-start-cmd`, `--ps-computed-start-cmd` | Override start command for buildpack apps |
| `stop-timeout-seconds` | app + global | `30` | `--ps-stop-timeout-seconds`, `--ps-global-stop-timeout-seconds`, `--ps-computed-stop-timeout-seconds` | Seconds Docker waits before SIGKILLing a container on stop |
### Read-only flags
The following flags surface in `ps:report` but are not managed by `ps:set` - they are derived from the running process state:
| Flag | Description |
|---|---|
| `--ps-can-scale` | `false` when the app's procfile or builder forbids horizontal scaling |
| `--deployed` | `true` after the first successful deploy |
| `--running` | `true` while any container for the app is running |
| `--processes` | Total scaled process count across all proctypes |
| `--status-<proctype>` | Container status and ID for each running proctype (one entry per Procfile process type) |
cron:list <app> [--format json|stdout] # List scheduled cron tasks for an app
cron:report [<app>] [<flag>] # Display report about an app
cron:resume <app> <cron_id> # Resume a cron task
cron:run <app> <cron_id> [--detach] # Run a cron task on the fly
cron:set [--global|<app>] <key> <value> # Set or clear a cron property for an app
cron:suspend <app> <cron_id> # Suspend a cron task
cron:list <app> [--format json|stdout] # List scheduled cron tasks for an app
cron:report [<app>] [<flag>] # Display report about an app
cron:resume <app> <cron_id> # Resume a cron task
cron:run <app> <cron_id> [--detach] [--ttl-seconds SECONDS] # Run a cron task on the fly
cron:set [--global|<app>] <key> <value> # Set or clear a cron property for an app
cron:suspend <app> <cron_id> # Suspend a cron task
```
## Usage
@@ -43,7 +43,7 @@ A cron task takes the following properties:
Zero or more cron tasks can be specified per app. Cron tasks are validated after the build artifact is created but before the app is deployed, and the cron schedule is updated during the post-deploy phase.
Cron tasks can run for a maximum of 24 hours via the docker-local scheduler, after which they are reaped from the system.
Cron tasks can run for a maximum of 24 hours, after which they are reaped from the system. The `docker-local` scheduler reaps expired tasks via the `dokku ps:retire` pass that runs every 5 minutes, so a task may overrun its deadline by up to 5 minutes. The `k3s` scheduler enforces the deadline directly through the job's `activeDeadlineSeconds`.
See the [app.json location documentation](/docs/advanced-usage/deployment-tasks.md#changing-the-appjson-location) for more information on where to place your `app.json` file.
@@ -57,9 +57,35 @@ When running scheduled cron tasks, there are a few items to be aware of:
- A `MAILTO` value can be set via the `cron:set` command.
- A `MAILFROM` value can be set via the `cron:set` command.
- Each scheduled task is executed within a one-off `run` container, and thus inherit any docker-options specified for `run` containers. Resources are never shared between scheduled tasks.
- Scheduled cron tasks are supported on a per-scheduler basis, and are currently only implemented by the `docker-local` scheduler.
- Tasks for _all_ apps managed by the`docker-local` scheduler are written to a single crontab file owned by the `dokku` user. The `dokku` user's crontab should be considered reserved for this purpose.
- Scheduled cron tasks are supported on a per-scheduler basis. Schedulers that use the host crontab - such as `docker-local` - have their `app.json` cron tasks written to the `dokku` user crontab, while schedulers that manage their own cron backend - such as `k3s` - schedule them natively.
- Tasks for _all_ apps managed by a host-crontab scheduler such as`docker-local` are written to a single crontab file owned by the `dokku` user. The `dokku` user's crontab should be considered reserved for this purpose.
- The `command` is tokenized and exec'd directly inside the container. Shell features such as `;`, `&&`, `|`, and `>` are _not_ interpreted. Commands that contain a bare shell operator are rejected when `app.json` is validated at deploy time, so a malformed cron command will fail the deploy rather than silently fail to run. If shell semantics are required, wrap the command explicitly, for example `"sh -c 'do-thing > /var/log/x.log'"`.
- Task output is written to the container's stdout and stderr, and can be persisted via Dokku's [vector integration](/docs/deployment/logs.md#configuring-a-cron-task-log-sink). See [persisting cron task output](#persisting-cron-task-output) below.
- A cron task cannot declare a log file path in `app.json`. The crontab written for the `dokku` user contains only `dokku cron:run <app> <cron_id>` lines, and no path from a deployed repository is ever interpolated into it.
#### Persisting cron task output
Without further configuration, a task's output is only delivered to the `MAILTO` address configured for cron. To retain it, configure a sink via Dokku's [vector integration](/docs/deployment/logs.md#vector-logging-shipping).
Any sink configured for the app already receives cron task output alongside the app's other logs:
To write it to a file on the host, target the `/var/log/dokku/apps` directory, which is mounted into the vector container. The `dokku_cron_id` field is available for templating, so each task can be given its own file:
See [configuring a cron task log sink](/docs/deployment/logs.md#configuring-a-cron-task-log-sink) for the routing rules, the available fields, and the caveat around very short-lived tasks.
### Changing cron management settings
@@ -68,7 +94,7 @@ The `cron` plugin provides a number of settings that can be used to managed depl
An on-the-fly invocation runs for a maximum of 24 hours (86400 seconds), the same deadline scheduled invocations receive. A different deadline can be requested with the `--ttl-seconds` argument:
The value applies only to that invocation - tasks started by the schedule keep the 24 hour default.
All one-off cron executions have their containers terminated after invocation.
#### Displaying reports
@@ -295,3 +329,17 @@ SHELL=/bin/bash
### PLACE ALL CRON TASKS ABOVE, DO NOT REMOVE THE WHITESPACE AFTER THIS LINE
```
## Properties
### Settable properties
> [!NOTE]
> The `Report flags` column lists the CLI argument names accepted by `cron:report`. The JSON keys emitted by `cron:report --format json` are the same names with the leading `--cron-` stripped (e.g. `global-mailto`, `computed-mailto`, `maintenance`). Legacy keys with the `cron-` prefix (e.g. `cron-global-mailto`) are also emitted during the 0.38.x deprecation window and will be removed in a future major release.
| `mailfrom` | global only | none | `--cron-global-mailfrom`, `--cron-computed-mailfrom` | `From:` address used in cron failure emails |
| `mailto` | global only | none | `--cron-global-mailto`, `--cron-computed-mailto` | Recipient address for cron failure emails; empty disables email |
| `maintenance` | app + global | `false` | `--cron-maintenance`, `--cron-global-maintenance`, `--cron-computed-maintenance` | When `true`, suspends all cron tasks for the app (or globally) |
| `maintenance.<cron-id>` | app only | `false` | `--cron-maintenance-<cron-id>` (dynamic per task) | Suspends an individual cron task by its computed ID (one row per task); written by `cron:suspend`/`cron:resume` |
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.