# 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`.
The release_and_deploy function and several proxy plugin scripts call fn-plugin-property-* helpers without sourcing `plugins/common/property-functions`, producing `command not found` errors during deploys. Source the helpers from `plugins/common/functions` and from each proxy plugin entry point that consumes them.
The ppa:cncf-buildpacks/pack-cli launchpad source is currently down, breaking the runtime image build and any bats run that calls install_pack. Pack is now tracked in contrib/dependencies.json and pulled from the buildpacks/pack GitHub release tarball, matching how other binary deps are managed. update-deb-dependencies gains hyphen-aware word-boundary matching so single-word names like pack cannot accidentally rewrite unrelated debian/control lines, keeping pack out of the package's hard requirements.
`SaveEntry` and `touchMigrationFlag` wrote files via `os.WriteFile` and `os.Create` and never chowned them, so the install-time legacy-mount migration produced root-owned `legacy-*.json` files in `/var/lib/dokku/data/storage-registry/entries/`. The dokku user that runs `ps:rebuild` could not read them and every rebuild on a 0.37.x to 0.38.0 upgrade failed with permission denied. The companion `repairRegistryOwnership` pass in `TriggerInstall` rewrites ownership across the whole registry tree so installs that already ran the buggy code are fixed on the next package upgrade, since the per-app migration flag would otherwise cause `MigrateLegacyMounts` to skip the broken files forever.
The previous regression case selected `--openresty-global-hsts`, but `hsts` is rejected by `openresty:set --global` because it is not in the plugin's GLOBAL_KEYS list. Switch to `--openresty-letsencrypt-server`, which the bats setup already pins to a known staging URL, so the test exercises the same `--global --<info-flag>` parsing path without needing a separate set step.
The argument-handling block in each plugin's report command overwrote APP with --global before checking whether $1 actually held an info flag, so `dokku <plugin>:report --global --<plugin>-foo` always emitted the full report instead of the requested value. Reorder the conditionals across the sixteen plugins that share the pattern so the info-flag check runs first, and add a regression bats case for each one.
The byjg/easy-haproxy image polls Docker for label changes every 10
seconds by default, which races with the haproxy bats suite and
intermittently produces curl exit 7. Expose `refresh-conf` as a
global-only haproxy property that maps to `EASYHAPROXY_REFRESH_CONF`,
lower it to 2 seconds in the bats setup, and wrap the localhost HTTP
assertions in a retry loop so checks wait for haproxy to converge
rather than failing on the first attempt.
The 0.38.0 migration documents `proxy:set <app> type <value>` as the canonical way to set the proxy implementation, but several user-facing examples still taught the legacy implicit form. Switch every example over to the explicit property syntax so the docs match the migration guide and other property-based plugin commands.
Per-plugin management docs now describe the properties introduced by the env-var-to-property migration in PR #8498, and stale prose and command-output examples that still referenced the old `DOKKU_*` names have been refreshed. The deprecated env vars table moves out of `environment-variables.md` and into the 0.38.0 migration guide, where it functions as a one-time pointer for upgrading users rather than ongoing reference material.
The default nginx template rendered four near-identical server blocks per app, so the same listen, access_log, error_log, ssl_*, error_page, and proxy chain had to be maintained across http, https, grpc, and grpcs branches. Merging http and https into a single branch keyed on an `is_ssl` boolean, and likewise grpc and grpcs, removes the duplicate proxy_set_header chain and error-page locations and brings the structure in line with the openresty proxy template. Output is preserved up to whitespace and the existing `cat -s` pass already squashes the leftover blank lines. Adds plugins/nginx-vhosts/template_test.go exercising the rendered output via sigil as a Go library across HTTP-only, HTTPS, HTTP-to-HTTPS redirect, no-listeners 502 fallback, gRPC, gRPCs, gRPC-without-listeners skip, IPv4 bind, upstream blocks with and without keepalive, the access_log `off` short-circuit, the X-Forwarded-Ssl toggle, the http2 listen-parameter vs directive split, http2_push_preload conditional emission, and the nginx.conf.d/*.conf include in every code path; and tests/unit/nginx-vhosts_16.bats covering deploy plus `nginx -t` end-to-end for both HTTP and HTTPS apps.
# History
## 0.38.0
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.0/bootstrap.sh
sudo DOKKU_TAG=v0.38.0 bash bootstrap.sh
```
See the [0.38.0 migration guide](/docs/appendices/0.38.0-migration-guide.md) for more information on migrating to 0.38.0.
### Bug Fixes
- #8533: @josegonzalez Split env config and image pull secret into separate helm releases
- #8530: @josegonzalez Split multi-flag input in docker-options
- #8528: @josegonzalez Skip retiring images still in use by app containers
- #8525: @josegonzalez Add launcher entrypoint for CNB images on dokku run and cron:run
- #8522: @josegonzalez Only emit keda fallback when a non-cpu/memory trigger exists
- #8515: @josegonzalez Fix vector mount directory config
- #8508: @josegonzalez Preserve all domains when renaming an app
- #8507: @josegonzalez Retire orphaned containers when scaling down
### New Features
- #8538: @josegonzalez Add scheduler-aware named storage entries
- #8527: @josegonzalez Accept --global on :report subcommands
- #8524: @josegonzalez Pre-validate custom nginx.conf.sigil during core-post-extract
- #8523: @josegonzalez Support resource limits on the build container
- #8517: @josegonzalez Send SIGTERM to old containers immediately on deploy
- #8516: @josegonzalez Scope docker-options to specific procfile processes
- #8509: @josegonzalez Ship default catch-all site on fresh apt install
- #8506: @josegonzalez Add --format json to git:report and nginx:report
- #8505: @josegonzalez Add git:auth-status to check netrc match
- #8493: @josegonzalez Generate 502 config for apps without web listeners
- #8404: @josegonzalez Upgrade vector chart from 0.42.0 to 0.52.0
- #8403: @josegonzalez Upgrade ingress-nginx chart from 4.10.0 to 4.15.1
- #8402: @josegonzalez Upgrade keda to 2.19.0 and keda-add-ons-http to 0.12.2
- #8259: @josegonzalez Add post-create support for env key in app.json
- #8157: @josegonzalez Add support for specifying buildpacks via app.json
- #8154: @josegonzalez Enable live-restore by default when installing Dokku
- #3697: @josegonzalez Migrate builds plugin to go and track per-build records
### Refactors
- #8514: @josegonzalez Migrate docker-options subcommands to go
- #6716: @josegonzalez Move app and global ENV files to consolidated config path
### Dependencies
- #8541: @dependabot[bot] chore(deps): bump traefik from v3.6.14 to v3.6.15 in /plugins/traefik-vhosts
- #8537: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.43 to 2.11.44 in /plugins/scheduler-k3s
- #8535: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.39.1 to 1.40.0 in /plugins/common
- #8529: @josegonzalez chore: bump dokku/netrc to v0.11.0
- #8520: @dependabot[bot] chore(deps): bump packaging from 26.1 to 26.2 in /docs/_build
- #8510: @dependabot[bot] chore(deps): bump packaging from 26.1 to 26.2 in /docs/_build
- #8503: @josegonzalez Bump dependency versions and add daily updater workflow
- #8502: @josegonzalez Bump go version to 1.26.2
- #8495: @dependabot[bot] chore(deps): bump k8s.io/apimachinery from 0.35.4 to 0.36.0 in /plugins/scheduler-k3s
- #8494: @dependabot[bot] chore(deps): bump dokku/openresty-docker-proxy from 0.9.3 to 0.10.0 in /plugins/openresty-vhosts
- #8490: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.35.4 to 1.36.0 in /plugins/scheduler-k3s
### Other
- #8498: @josegonzalez Migrate environment variables to plugin properties
Adds plugins/storage/migrate_test.go exercising migrateApp and MigrateLegacyMounts: single deploy phase, cross-phase grouping into one attachment, :ro and free-form volume options preserved on the attachment, idempotency on a second pass, the per-app flag-file fast path that skips already-migrated apps, and the name-collision refusal that aborts before draining. tests/unit/storage.bats gains an end-to-end case that stages a -v line via docker-options:add, runs storage:migrate, and asserts the synthesized colon form shows up in storage:list, the legacy-<hash> entry shows up in storage:list-entries, and the original -v line is gone from docker-options:report on both phases. To make that test (and operators with restored backups) practical, storage gains a storage:migrate <app>|--all subcommand backed by a new MigrateApp helper that skips the per-app flag file so a re-run actually re-scans.
Adds typed JSON build records under data/builds/<app>/<build-id>.{json,log} keyed on a stable base36 ULID-style DOKKU_BUILD_ID generated for every deploy. The new commands surface that history (builds:list, builds:info, builds:prune) and an operator-configurable retention via builds:set retention. The existing builds:cancel and builds:output now key on the build-id (with safe handling for already-finalized and abandoned records), and the per-build log file replaces journalctl as the durable source of truth for builds:output.
CallExecCommand wraps non-zero exit codes as errors with the code populated on the response, so storage:exec was returning the wrapped error and being collapsed to exit 1 by LogFailWithError before the os.Exit branch ran. Both storage's CommandExec and scheduler-docker-local's TriggerSchedulerStorageExec now check ExitCode before err so the underlying tool's status flows through verbatim. Separately, the legacy host:container colon form of storage:mount used to write straight into docker-options, but storage:list now reads only attachments, so newly mounted legacy-form storage was invisible. CommandMount and CommandUnmount route the colon form through LegacyMountToEntry plus AddAttachment / RemoveAttachment, making storage:list show every mount regardless of form while preserving the existing "Mount path already exists." / "Mount path does not exist." error wording.
The install trigger created /var/lib/dokku/data/storage-registry and its entries / migrations subdirectories as root with mode 0755, so the dokku user that runs storage:create couldn't write entry JSON files or migration flag files into them. Mirrors what PropertySetup does for config/storage by calling SetPermissions on each registry path. This unblocks the bats coverage that was failing at the first storage:create call.
Moves the actual exec out of the storage plugin and into a new scheduler-storage-exec plugn trigger. scheduler-docker-local does docker run with TTY-aware -it/-i selection and --user derived from entry.Chown; scheduler-k3s creates a throwaway Pod via the kubernetes API, waits for it to reach Running with structured error reporting (ImagePullBackOff and friends are surfaced from the container status verbatim, no kubectl involvement), execs the user command via the existing SPDY plumbing in k8s.go, and deletes the Pod on the way out. (Entry).Validate now accepts either an absolute path or a docker named-volume token for docker-local entries so the migration synthesizer's named-volume legacy entries work cleanly. storage:exec gains --as-user for one-off uid overrides, propagates the underlying tool's exit code via os.Exit, and detects TTY/interactive mode from os.Stdin so non-interactive scripted use no longer trips over docker's input-device-is-not-a-tty error.
storage:list was calling the storage-list plugn trigger which read -v lines from docker-options. After the install-time migration drains those lines into the attachment store, that source is empty for every migrated app and for any app that only ever used storage:create + storage:mount. The fix moves CommandList to call a new in-process ListAppMountEntries helper that reads attachments directly, surfaces the entry name in JSON output via a new entry_name field, and falls back to the entry name as the host token for k3s entries with no host path so the colon form remains well-formed. The storage-list plugn trigger is kept for back-compat with external callers but emits a deprecation warning and now reads from the same attachment-driven source.
The previous condition used a bash regex `git-*` that matched git-receive-pack and git-upload-pack, interposing a tee chain on the binary git wire protocol and producing `fatal: protocol error: bad line length character: !` on every push. Restrict the redirect to the pre-receive hook, where tee passthrough is safe because git-receive-pack forwards hook output via the protocol sideband.
Switches the storage plugin's post-delete and post-app-rename-setup triggers from rewriting the attachment list as empty to removing the per-app property folder outright via PropertyDestroy. This matches the convention other plugins use on app deletion and avoids leaving an empty mounts file behind for an app that no longer exists.
apps:destroy was failing in CI because the post-delete trigger called PropertyListWrite, which tried to mkdir /var/lib/dokku/config/storage/<app> as the dokku user but the parent directory had been created by the install trigger as root. The install trigger now calls PropertySetup("storage") so the per-plugin config root has the correct ownership, and post-delete / post-app-clone / post-app-rename short-circuit when the app has no attachments to begin with so they can't fail on a never-touched property tree. The entry registry and migration flag directories also moved out of config/storage (where they would have collided with the property-store path config/storage/<appName>) into a dedicated data/storage-registry tree.
Updates persistent-storage.md to lead with the named storage entry workflow while keeping the legacy colon-form documentation intact, adds a Persistent storage section to the k3s scheduler doc, documents the storage-app-mounts, storage-create, storage-destroy, and storage-status triggers in plugin-triggers, and adds an entry to the 0.38.0 migration guide explaining the install-time migration of legacy mounts and the new DNS-1123 name validation. Bats coverage in tests/unit/storage.bats now exercises storage:create / list-entries / destroy, name validation rejections, multi-entry attachment, the destroy-while-mounted error, and the ensure-directory deprecation warning.
storage:set is the in-place editor for an existing entry; size grows are passed through to a helm upgrade and Kubernetes-incompatible swaps (access-mode, storage-class) are rejected up front. storage:exec spawns a throwaway container that mounts the entry: docker run on docker-local entries, kubectl run with a Pod-spec override on k3s, falling back to bash then sh when no command is supplied. storage:wait blocks until a k3s entry's PVC reports Bound via the storage-status trigger, with a docker-local fast path that just checks the host directory. storage:report --global enumerates every registered entry and the apps that mount it, in text or JSON form.
Adds a per-entry storage helm chart that owns the PVC and (when host_path is set) the PV, and three triggers for the storage plugin to drive: storage-create installs/upgrades the chart with storage-class existence validation, storage-destroy uninstalls it, storage-status reports the PVC phase. App deployment and cron-job charts now consume storage-app-mounts and reference each PVC by name; the // todo: implement volumes blocks are gone. ProcessVolume gained sub_path, read_only, and persistent_claim fields and the deployment/cron-job templates render them.
storage:mount and storage:unmount now accept the named-entry form (--container-dir, --phase, --process-type, --volume-subpath, --volume-readonly, --volume-chown), persisting attachments via the property list. The legacy host:container colon form is preserved for back-compat. Storage now emits its own docker-args-deploy and docker-args-run triggers so docker-local apps continue to receive bind mounts even after their data moves out of docker-options. The install trigger walks every app once per upgrade and converts legacy -v lines into legacy-<hash> entries plus attachments, draining the originals from docker-options last so a partial run leaves the old behavior intact. Lifecycle triggers post-delete, post-app-clone-setup, and post-app-rename-setup keep the attachment store in sync with app state.
Wires the new entry-management commands on top of the Entry type. storage:create accepts both docker-local entries (with idempotent host-directory creation) and k3s entries (which delegate PVC provisioning to a forthcoming scheduler-side storage-create trigger). storage:destroy refuses to remove an entry that any app still mounts. storage:ensure-directory now emits a deprecation warning. storage:mount and storage:unmount keep the existing colon-form behavior so this commit is non-breaking; the named-entry mount form follows in a later commit.
Introduces the Entry and Attachment types that the storage plugin will use as the source of truth for named storage volumes, replacing the colon-delimited mount strings stored under docker-options. Entries persist as JSON under config/storage/entries and validate against scheduler-specific rules; attachments persist via the property list and capture how an app uses an entry. The legacy migration name synthesizer is also added so existing colon-form mounts can converge on a deterministic legacy- entry name. Existing storage commands are unchanged in this commit.
This still needs tightening to ensure we don't trigger this for normal git plugin commands - as well as anything that triggers a deploy - but works in a pinch.
The DOKKU_PID now never gets overwritten except in the case that DOKKU is executed by the sudo user. If the command ends up executing a deploy, then the pid of the `dokku` owned process - which may have been executed via sudo - will be written to the file lock, allowing future commands to interact with the original process.
Additionally, the new builds plugin can be used to handle killing a build.
Multi-flag inputs (e.g. `--build-arg X=Y --link a --link b`) used to be stored as a single line, which bypassed the per-line filter that drops `--link` and similar flags for dockerfile-based builders. Each `--flag [value]` group is now stored as its own entry, and a `--process` typed after the app name is lifted into the subcommand flag instead of being stored as a docker option.
The new config and pull secret helm releases need to honor user-set annotations and labels for `--resource-type secret` so existing scheduler-k3s annotation tests keep passing. The dedicated charts now render `.Values.global.annotations` and `.Values.global.labels` onto their Secret manifest, and the deploy trigger plumbs `SecretAnnotations` and `SecretLabels` from the global annotation/label config. The rollback regression bats test now uses `dokku ps:rebuild` for its second deploy because git push of an unchanged ref is rejected by the dokku remote.
Bundling these Secrets in the app helm chart caused two bugs in the scheduler-k3s plugin: a chart rollback could delete Secrets that older ReplicaSets still referenced by exact timestamped name (`env-{app}.{ts}` and `ims-{app}.{ts}`), hard-crashing pods until manual intervention; and the strategic-merge `patchMergeKey` on `imagePullSecrets` let stale entries leak into the live Deployment until the list pointed at many nonexistent Secrets. Each Secret now lives in its own helm release with a stable name (`config-{app}` and `pull-secret-{app}`), installed before the app chart on every deploy. The deployment trigger also prunes any leaked `imagePullSecrets` entries from the live Deployment so the next deploy lands on a clean list, and the rename and destroy paths uninstall the new releases (and the previously-leaked TLS release on rename) under the old app name.
Every `:report` subcommand now recognizes `--global` as a scope selector that limits the report to globally-configured properties, including in JSON form via `--global --format json`. Previously this combination was rejected because `--global` was treated as an info flag, conflicting with `--format`. The shared `common.ParseReportArgs` helper now returns a `ReportArgs` struct exposing the parsed scope; each Go and bash report selects a global-only flag map when scope is global, and skips per-app verification.
The previous assertion piped grep -c through `|| echo 0`, which printed an extra `0` when the file existed without matches because grep -c emits the count of `0` and exits 1, triggering the fallback.
The `git:auth` test in `tests/unit/git_3.bats` verifies the written entry with `netrc get --netrc-file ${DOKKU_ROOT}/.netrc github.com`, but the `--netrc-file` flag was added in netrc v0.11.0 and CI was still pinned to v0.10.3, so the assertion ran against root's `~/.netrc` and exited 1.
The trigger output started with `--entrypoint` and relied on the caller
to provide a trailing space. `scheduler-deploy-process-container` does
that, but `scheduler-run` does not when `DOKKU_TRACE` is unset, so the
flag concatenated onto the previous arg as `--env=KEY--entrypoint`,
leaving `launcher` as the image positional argument and causing
`docker container create` to fail with `Unable to find image 'launcher:latest'`.
Match the leading-space convention used by the other docker-args triggers
(`config/docker-args-run`, `builder-herokuish/docker-args-run`).
The scheduler-run script classified CNB-based images as `herokuish` because
`is_image_herokuish_based` returns true for them, which caused the
`docker-args-process-run` trigger for builder-pack to skip injecting
`--entrypoint launcher`. Without that flag the container fell back to the
image entrypoint (`/cnb/process/web`) and dropped the user-supplied
arguments. Mirror the deploy-side detection so CNB images set
`IMAGE_SOURCE_TYPE=pack`, allowing the launcher entrypoint to be added.
When ps:rebuild runs against an image-based deploy via git:from-image, the resulting image often shares the same SHA as the previous deployment, so retiring the old container's image would target the live image of the new container. The retirement is now skipped when another running container of the same app still references the image, and the cron retire loop self-heals previously stuck entries the next time it encounters them.
When pre-validating a custom nginx.conf.sigil before the build phase, no app listeners exist yet on first deploys. Templates that emit `proxy_pass http://app-port` while gating the matching upstream block on `DOKKU_APP_WEB_LISTENERS` render an undefined upstream, causing `nginx -t` to fail with "host not found in upstream". Pre-validation now passes a `127.0.0.1:5000` placeholder for `DOKKU_APP_WEB_LISTENERS` so the upstream block emits a static server entry and the template can be validated for syntax without depending on live listeners.
Renders the user-supplied nginx.conf.sigil via sigil into a tmp file and runs `nginx -t` against a wrapped copy as soon as the template is extracted from the source tree, so syntactically invalid templates abort the deploy before the build phase runs. Skipped when `proxy-type` is not `nginx`, when `disable-custom-config=true`, or when no custom template was extracted. Closes#7827.
Adds a `docker-args-process-build` trigger to the resource plugin so
limits set via `dokku resource:limit --process-type build APP` are
applied during the build phase. Only `build.limit.*` properties are
read - defaults do not inherit, since builds typically need more memory
than runtime and a leaked tiny default would cause confusing OOM
failures. Reservations are never applied at build time. Allowed flags
are filtered per builder: herokuish gets cpu, memory, memory-swap, and
nvidia-gpu; dockerfile gets memory and memory-swap; pack, nixpacks,
railpack, lambda, and null emit nothing because their underlying CLIs
do not accept docker run resource flags. The dockerfile builder
whitelists the new memory flags and corrects pre-existing typos where
`--ssh` mapped to `--platform` and `--ulimit` mapped to `--tag`.
The bats test for migration idempotency invoked the install binary directly via sudo, which kept hitting fresh missing env vars on each iteration (DOKKU_LIB_ROOT, then PLUGIN_PATH, etc). The dokku launcher script exports a chain of vars that the install path reads via common.MustGetEnv, and mirroring that chain in a bats test is fragile. Migration is pure file IO plus a property marker - perfect for a Go unit test that isolates itself with t.TempDir and t.Setenv. The new test covers parsing comments and blank lines, the marker-based no-op on re-run, and the rule that a manually re-created legacy file is left untouched after the marker is set.
The ps plugin auto-adds --restart=on-failure:10 to the default deploy scope on app create, so docker-options:list --phase deploy is never empty for a freshly-created app. Switch the default-scope assertions from exact-match to contains/not-contains so the auto-injected restart line stops causing false failures. The migration test invoked the install binary directly via sudo, but DOKKU_LIB_ROOT is normally exported by the dokku launcher script and isn't set in that subprocess; pass it (and DOKKU_ROOT) explicitly.
Keda 2.17+ rejects ScaledObjects whose spec.fallback is set unless at least one trigger is not a cpu or memory scaler, so unconditionally emitting fallback broke deploys for apps autoscaled on cpu or memory alone. The chart now skips the fallback block when every configured trigger is cpu or memory and keeps the existing behavior otherwise.
The :list subcommand had `SetInterspersed(false)` which forced flags to come before positional arguments; in practice users invoke it as `docker-options:list <app> --process X --phase Y`. Drop the non-interspersed setting so flags can appear after the app name. Two existing buildpacks/dockerfile tests appended directly to the legacy `DOCKER_OPTIONS_DEPLOY` file - that path is no longer the source of truth, so switch them to `docker-options:add`. The new migration test invoked `dokku plugin:trigger install` which fans out to every plugin and trips on unrelated permission errors; call the docker-options install binary directly instead. Tighten the report and JSON assertions to match the actual output format (`.` becomes a space in the display key, and JSON values may concatenate options).
The docker-local scheduler now sends `SIGTERM` to old containers immediately after a successful deploy via `docker container kill --signal=SIGTERM`, 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 existing `wait-to-retire` grace period and `stop-timeout-seconds` hard-stop continue to apply unchanged as the authoritative cleanup path.
Adds a `--process` flag (repeatable) to docker-options:add/remove/clear/list and the new docker-options:list subcommand for querying a single process+phase pair. Process scoping is supported only for the deploy phase since build runs once per app and run covers ad-hoc commands and cron tasks where no Procfile process type is available. Storage moves from `$DOKKU_ROOT/$APP/DOCKER_OPTIONS_*` files to property lists under `/var/lib/dokku/config/docker-options/$APP/{processType}.{phase}`, with `_default_` as the sentinel for app-wide options. The install trigger migrates pre-existing DOCKER_OPTIONS_* files into property lists once and renames them to `.migrated`; a global marker makes re-runs strictly no-op. The legacy docker-args-{build,deploy,run} bash triggers are reimplemented in Go alongside a new docker-args-process-deploy trigger that surfaces per-process options to the scheduler. The :report command exposes one dynamic flag per configured `process.deploy` pair (e.g. `--docker-options-deploy.web`) and supports `--format json`. There is no `--global` flag; omitting `--process` keeps the historical default behaviour, since `--global` elsewhere in dokku means "across all apps". Closes#2441.
The single-udp network test was calling `docker-options:add $TEST_APP -p 1194:1194` without a phase argument. The bash subcommand silently no-op'd because `-p` is not a recognized phase, so the test happened to pass even though it was not actually setting the option. The go port surfaces the validation error correctly, so the test now needs the proper `deploy` phase to keep working.
Re-enabling vhost on an app that was previously deployed with no global VHOST left the auto-assigned random high port behind in the ports plugin's `map-detected` property, so nginx kept listening on that port and the new catch-all default vhost rejected requests for the app's own domain on port 80.
Re-enabling vhost (via `proxy:enable`, `domains:enable`, or `domains:add` with no global VHOST) was leaving the app pinned to the random high port and the empty VHOST file from the previous disabled state, so the new catch-all vhost intercepted requests for the app's domain. The `domains-enable` trigger now restores default vhosts via `domains_enable`, and `pre-enable-vhost` clears any stored proxy ports so they can be recomputed to 80/443. Also fixes the netrc lookup in the `git:auth` test to read dokku's netrc file directly.
Renaming `/etc/nginx/sites-enabled/default` to `.dokku-disabled` left a broken symlink that nginx's `include /etc/nginx/sites-enabled/*;` glob still picked up, breaking config validation. Files inside `sites-enabled/` are now removed; the real config in `sites-available/default` is still preserved via rename.
Fresh apt installs now drop a catch-all server block at `/etc/nginx/conf.d/00-default-vhost.conf` that uses `ssl_reject_handshake on` and `return 444` to drop requests with unknown Host headers. Conflicting upstream nginx default vhosts are renamed to `*.dokku-disabled` rather than deleted, preserving any local edits. The new `dokku/install_default_site` debconf flag opts out of the install. Upgrades leave existing nginx config untouched.
Reloads the nginx server config after validating it with `nginx -t`. Useful for picking up changes to globally managed nginx configs without restarting the running process.
Ports the user-facing CLI subcommands and lifecycle triggers from bash to go, mirroring the layout used by the ports plugin. The on-disk format (`DOCKER_OPTIONS_BUILD/DEPLOY/RUN` files), the `docker-args-build/deploy/run` triggers, and every exported helper consumed by other plugins are unchanged. Reserves the `--process` and `--global` flags on add, remove, and clear so usage today exits with a not-yet-implemented error and the surface is locked in ahead of the process-scoping work tracked in #2441.
Some helm chart upgrades require side-effects that a plain `helm upgrade` cannot perform, such as deleting deployments whose immutable selectors changed. Charts may now register pre and post upgrade hooks against a target version; applicable hooks fire in ascending semver order, each bracketed around an upgrade to its version, with a final upgrade to the chart's configured version when needed. The new `keda-add-ons-http` 0.12.2 hook deletes the chart-managed deployments before the upgrade so the new selectors take effect cleanly.
The intermediate container count check raced with the existing
zero-downtime retire of the previous container, so docker ps still
saw the renamed-but-pending container until wait-to-retire elapsed.
When scaling a process type to a smaller count, the indices above the
new count had their `CONTAINER.<proctype>.<idx>` state files deleted but
the underlying Docker containers were never registered for retirement,
so they remained running indefinitely. The cleanup block now reads each
orphaned state file and registers its container id with the existing
`scheduler-register-retired` trigger before the file is removed.
The `post-app-rename-setup` trigger redirected `sed` output to the new
app's VHOST file inside a loop over global vhosts, so each iteration
clobbered the previous one and only the last global subdomain was
rewritten. When no global vhost was set, the loop never ran and the
new app's VHOST file was never written, leaving the renamed app with
no domains.
Copy the old app's VHOST file once as a baseline, then apply `sed -i`
in place for each global vhost so substitutions accumulate. Guard
against a missing global VHOST file and skip blank lines.
- config_test.go: create app dir at DOKKU_ROOT for VerifyAppName and
ensure global config parent directory exists
- migrateGlobalEnv: set dokku:dokku ownership on global ENV file after
writing so deploy processes running as dokku can read it
The post-create trigger only created the property directory at
DOKKU_LIB_ROOT/config/config/{app}/, but getAppFile() writes the
ENV file to DOKKU_LIB_ROOT/config/{app}/ENV. Add setupAppConfigDir
to create and permission the ENV directory in both post-create and
install triggers, and clean it up on post-delete.
- Move migrateGlobalEnv() before UnfilteredDokkuApps() so the global
config directory is created even on fresh installs with no apps
- Add DOKKU_LIB_ROOT to Go test env vars in tests.mk
- Use DOKKU_LIB_ROOT instead of DOKKU_ROOT in getGlobalFile() to match
the intended new config path layout
- Add missing comma in filepath.Join for old app ENV migration path
- Revert common_test.go app dirs to DOKKU_ROOT since app detection
still uses AppRoot()
Mirrors the JSON output convention already used by scheduler:report, builder:report, network:report, and traefik:report. Both plugins now accept `--format json` to emit a single-line JSON object whose keys are the property names with the plugin prefix stripped. Combining `--format` with an info flag is rejected with an error message that matches the Go-based report helper.
Closes#8499
Adds `git:auth-status HOST [USERNAME] [PASSWORD]` which exits 0 when the
configured `.netrc` entry matches the requested state and 1 otherwise,
allowing external tooling to detect whether `git:auth` would change
anything without reading `$DOKKU_ROOT/.netrc` directly. Both `git:auth`
and `git:auth-status` now also accept the password via `STDIN`.
Pre-set LOCAL_DOMAIN in the tini test so mastodon's required env var
resolves, and replace unsupported `generator: "echo 5"` with `value: "5"`
in the nodejs-express and dockerfile fixtures so WEB_CONCURRENCY no
longer aborts deploys via the new app.json env processor.
Switches the dependency-updates workflow to use HOMEBREW_GITHUB_API_TOKEN
(the same PAT used to bump the homebrew formula on release) and sets
the committer/author to Dokku Bot <no-reply@dokku.com>.
Bumps every entry in contrib/dependencies.json to its latest upstream
release. Adds a scheduled GitHub Actions workflow that checks each
dependency once a day and opens one PR per outdated entry.
- Mount `/vector-data-dir` from host (`/var/lib/vector` via `hostPath`, `DirectoryOrCreate`) so `kubernetes_logs` checkpoints survive pod restarts; resolves conflict with the chart's read-only `/var/lib` default volume
- Enable Vector API on `:8686` and wire HTTP `/health` liveness and readiness probes; add `internal_metrics` source and `prometheus_exporter` sink on `:9090` so the `prom-exporter` service port is actually backed by an exporter
- Set explicit `targetPort` on the `prom-exporter` port to avoid inference mismatches
- Remove the `.message = .message` no-op
- Add `node-role.kubernetes.io/control-plane` toleration alongside the legacy `master` key so the DaemonSet schedules on control-plane nodes regardless of cluster age
# History
## 0.37.10
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.10/bootstrap.sh
sudo DOKKU_TAG=v0.37.10 bash bootstrap.sh
```
### Tests
- #8491: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 285 to 287 in /tests/apps/php
- #8478: @josegonzalez chore: label test app dependency updates as type: tests
### Dependencies
- #8486: @dependabot[bot] chore(deps): bump click from 8.3.2 to 8.3.3 in /docs/_build
- #8492: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.29.0 to 1.31.0 in /plugins/scheduler-k3s
- #8489: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.42 to 2.11.43 in /plugins/scheduler-k3s
- #8488: @dependabot[bot] chore(deps): bump timberio/vector from 0.54.0-debian to 0.55.0-debian in /plugins/logs
- #8487: @dependabot[bot] chore(deps): bump traefik from v3.6.13 to v3.6.14 in /plugins/traefik-vhosts
- #8483: @dependabot[bot] chore(deps): update markdown requirement from <3.11,>=3.2.1 to >=3.10.2,<3.11 in /docs/_build
- #8485: @dependabot[bot] chore(deps): bump k8s.io/kubectl from 0.35.2 to 0.35.4 in /plugins/scheduler-k3s
- #8484: @dependabot[bot] chore(deps): bump k8s.io/client-go from 0.35.2 to 0.35.4 in /plugins/scheduler-k3s
- #8482: @dependabot[bot] chore(deps): bump ruby from 4.0.2 to 4.0.3 in /tests/apps/dockerfile-entrypoint
- #8481: @dependabot[bot] chore(deps): bump psycopg2-binary from 2.9.11 to 2.9.12 in /tests/apps/dockerfile-release
- #8479: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.23.0 to 0.23.1 in /plugins/scheduler-k3s
- #8480: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.35.3 to 1.35.4 in /plugins/scheduler-k3s
proxy:set <app> <proxy-type> continues to work as before by detecting
when the first argument is not a known property name and treating it
as the type value. Also preserves the port mapping detection error
for inputs containing colons.
- Move proxy-port and proxy-ssl-port properties from ports plugin to
proxy plugin where they conceptually belong
- Generalize proxy:set to handle any property (matching ps:set pattern)
instead of only handling the type property
- Fix apps:set --global flag parsing (pflag treats --global as a flag,
not an argument — must be defined explicitly like ps:set does)
- Add missing property-functions source to scheduler-docker-local
shell scripts (scheduler-enter, scheduler-run, scheduler-deploy,
check-deploy, bin/scheduler-deploy-process-container)
- Update nginx-vhosts property references from ports to proxy plugin
- Update tests to use proxy:set for proxy-port properties
- Add apps:set subcommand for managing apps properties (mirrors
ps:set, scheduler:set, builder:set pattern)
- Update nginx-vhosts functions/pre-disable-vhost/pre-enable-vhost
to read proxy port from properties instead of config vars
- Fix test commands to use correct property-setting approaches:
prop binary for ports properties, apps:set for apps properties
The apps:clone --skip-deploy test expected 404 for the cloned app's
domain. With the new empty nginx config, undeployed apps now correctly
return 502 Bad Gateway instead of falling through to nginx's default.
The config:show and config:export tests now include DOKKU_PROXY_PORT
in expected output since ports-configure runs at app creation time.
Add sleep before HTTP assertion in nginx-vhosts_13 test to allow
nginx async reload to complete.
When nginx is stopped (e.g. during traefik/caddy tests), the
post-create trigger should not attempt to build and reload the
nginx config as the service is not running.
The post-create trigger was not executable so plugn skipped it.
Also skip generating the 502 config when PROXY_PORT_MAP is empty
(e.g. after ports:clear) to avoid a sigil template index error.
Merge the deployed and empty-listeners branches into a single flow
that shares the SIGIL_PARAMS array, temp dir setup, sigil rendering,
and nginx reload. Branch only for deployed-specific logic: image
verification, custom template lookup, per-process listeners, and HSTS.
Custom nginx.conf.sigil templates that reference DOKKU_APP_WEB_LISTENERS
may now receive an empty value when rendered for apps without running web
processes. The migration guide documents how to handle this with a
conditional in the template.
When an app has no web listeners (not yet deployed, no web process type,
or web processes not running), generate a minimal nginx config that
returns 502 Bad Gateway instead of having no config at all. This ensures
domains resolve, monitoring tools detect non-200 status codes, and SSL
certificate provisioning tools like letsencrypt can function.
Move dependabot entries for /tests/apps/ directories from the
"type: dependencies" label to "type: tests" so they appear under
the Tests section in the changelog instead of Dependencies.
When deploying via git:from-image, the source image may contain an
app.json with buildpacks entries that are not relevant. Check for
the source-image property and skip app.json buildpacks when set.
The buildpacks entry in tests/apps/dockerfile/app.json caused
the post-extract trigger to write a .buildpacks file during
Dockerfile-based deploys, breaking builder detection. Remove
the entry and add Go unit tests for getBuildpacks/validBuildpackURL,
a bats integration test for deploying with app.json buildpacks,
and documentation for the new feature.
# History
## 0.37.9
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.9/bootstrap.sh
sudo DOKKU_TAG=v0.37.9 bash bootstrap.sh
```
### Dependencies
- #8473: @dependabot[bot] chore(deps): bump k8s.io/api from 0.35.2 to 0.35.4 in /plugins/scheduler-k3s
- #8474: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.22.5 to 0.23.0 in /plugins/scheduler-k3s
- #8476: @dependabot[bot] chore(deps): bump github.com/go-acme/lego/v4 from 4.25.2 to 4.34.0 in /plugins/scheduler-k3s
- #8475: @dependabot[bot] chore(deps): bump github.com/moby/spdystream from 0.5.0 to 0.5.1 in /plugins/scheduler-k3s
# History
## 0.37.8
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.8/bootstrap.sh
sudo DOKKU_TAG=v0.37.8 bash bootstrap.sh
```
### Bug Fixes
- #8451: @mykolasolodukha Fix client prepending `--app` for non-app commands
### New Features
- #8443: @chemicalkosek Add application/graphql-response+json to nginx gzip_types
### Dependencies
- #8470: @dependabot[bot] chore(deps): bump packaging from 26.0 to 26.1 in /docs/_build
- #8471: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.35.0 to 1.35.3 in /plugins/scheduler-k3s
- #8472: @dependabot[bot] chore(deps): bump k8s.io/apimachinery from 0.35.2 to 0.35.3 in /plugins/scheduler-k3s
- #8413: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.24.0 to 1.28.0 in /plugins/scheduler-k3s
- #8425: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.19.3 to 1.20.0 in /plugins/scheduler-k3s
- #8469: @dependabot[bot] chore(deps): bump zipp from 3.23.0 to 3.23.1 in /docs/_build
- #8450: @dependabot[bot] chore(deps): bump mvdan.cc/sh/v3 from 3.13.0 to 3.13.1 in /plugins/cron
- #8465: @dependabot[bot] chore(deps): bump github.com/joho/godotenv from 1.2.0 to 1.5.1 in /plugins/config
- #8463: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.49.0 to 0.50.0 in /plugins/common
- #8449: @dependabot[bot] chore(deps): bump click from 8.3.1 to 8.3.2 in /docs/_build
- #8464: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.20.1 to 3.20.2 in /plugins/scheduler-k3s
- #8458: @dependabot[bot] chore(deps): bump traefik from v3.6.12 to v3.6.13 in /plugins/traefik-vhosts
- #8457: @dependabot[bot] chore(deps): bump golang from 1.26.1 to 1.26.2 in /tests/apps/zombies-dockerfile-no-tini
- #8456: @dependabot[bot] chore(deps): bump golang from 1.26.1 to 1.26.2 in /tests/apps/go-fail-postdeploy
- #8455: @dependabot[bot] chore(deps): bump golang from 1.26.1 to 1.26.2 in /tests/apps/gogrpc
- #8454: @dependabot[bot] chore(deps): bump golang from 1.26.1 to 1.26.2 in /tests/apps/zombies-dockerfile-tini
- #8453: @dependabot[bot] chore(deps): bump golang from 1.26.1 to 1.26.2 in /tests/apps/go-fail-predeploy
- #8452: @dependabot[bot] chore(deps): bump rack-session from 2.1.1 to 2.1.2 in /tests/apps/ruby
- #8459: @dependabot[bot] chore(deps): bump django from 5.2.12 to 5.2.13 in /tests/apps/dockerfile-release
- #8444: @dependabot[bot] chore(deps): bump rack from 3.2.5 to 3.2.6 in /tests/apps/ruby
- #8445: @dependabot[bot] chore(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 in /plugins/scheduler-k3s
- #8447: @dependabot[bot] chore(deps): bump werkzeug from 3.1.7 to 3.1.8 in /tests/apps/python-flask
- #8440: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.21 to 10.21.2 in /docs/_build
- #8441: @dependabot[bot] chore(deps): bump pygments from 2.19.2 to 2.20.0 in /docs/_build
- #8442: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.79.3 to 1.80.0 in /tests/apps/gogrpc
- #8410: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.48.0 to 0.49.0 in /plugins/common
- #8422: @dependabot[bot] chore(deps): bump github.com/fatih/color from 1.18.0 to 1.19.0 in /plugins/common
- #8434: @dependabot[bot] chore(deps): bump gunicorn from 25.2.0 to 25.3.0 in /tests/apps/python-flask
- #8439: @dependabot[bot] chore(deps): bump brace-expansion from 1.1.12 to 1.1.13 in /tests/apps/multi
- #8438: @dependabot[bot] chore(deps): bump path-to-regexp from 8.2.0 to 8.4.0 in /tests/apps/checks-root
- #8437: @dependabot[bot] chore(deps): bump gunicorn from 25.2.0 to 25.3.0 in /tests/apps/dockerfile-release
- #8436: @dependabot[bot] chore(deps): bump traefik from v3.6.11 to v3.6.12 in /plugins/traefik-vhosts
- #8435: @dependabot[bot] chore(deps): bump gunicorn from 25.2.0 to 25.3.0 in /tests/apps/multi
- #8427: @dependabot[bot] chore(deps): bump werkzeug from 3.1.6 to 3.1.7 in /tests/apps/python-flask
- #8428: @dependabot[bot] chore(deps): bump gunicorn from 25.1.0 to 25.2.0 in /tests/apps/multi
- #8429: @dependabot[bot] chore(deps): bump gunicorn from 25.1.0 to 25.2.0 in /tests/apps/python-flask
- #8430: @dependabot[bot] chore(deps): bump djangorestframework from 3.17.0 to 3.17.1 in /tests/apps/dockerfile-release
- #8431: @dependabot[bot] chore(deps): bump gunicorn from 25.1.0 to 25.2.0 in /tests/apps/dockerfile-release
- #8432: @dependabot[bot] chore(deps): bump picomatch from 2.3.1 to 2.3.2 in /tests/apps/multi
- #8423: @dependabot[bot] chore(deps): bump traefik from v3.6.10 to v3.6.11 in /plugins/traefik-vhosts
- #8420: @dependabot[bot] chore(deps): bump mkdocs-material from 9.7.5 to 9.7.6 in /docs/_build
- #8421: @dependabot[bot] chore(deps): bump importlib-metadata from 8.8.0 to 9.0.0 in /docs/_build
- #8424: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.40 to 2.11.41 in /plugins/scheduler-k3s
- #8411: @dependabot[bot] chore(deps): bump lucaslorentz/caddy-docker-proxy from 2.11 to 2.12 in /plugins/caddy-vhosts
- #8412: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.20.0 to 3.20.1 in /plugins/scheduler-k3s
- #8419: @dependabot[bot] chore(deps): bump djangorestframework from 3.16.1 to 3.17.0 in /tests/apps/dockerfile-release
- #8418: @dependabot[bot] chore(deps): bump importlib-metadata from 8.7.1 to 8.8.0 in /docs/_build
- #8417: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.77.0-dev to 1.79.3 in /plugins/scheduler-k3s
- #8416: @dependabot[bot] chore(deps): bump ruby from 4.0.1 to 4.0.2 in /tests/apps/dockerfile-entrypoint
- #8415: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.79.2 to 1.79.3 in /tests/apps/gogrpc
- #8409: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.22.4 to 0.22.5 in /plugins/scheduler-k3s
- #8408: @dependabot[bot] chore(deps): bump github.com/go-resty/resty/v2 from 2.17.1 to 2.17.2 in /plugins/scheduler-k3s
- #8407: @dependabot[bot] chore(deps): bump mkdocs-material from 9.7.4 to 9.7.5 in /docs/_build
- #8406: @dependabot[bot] chore(deps): bump timberio/vector from 0.53.0-debian to 0.54.0-debian in /plugins/logs
### Other
- #8468: @josegonzalez chore: bump dependencies in tests/apps/php
- #8467: @josegonzalez chore: upgrade traefik from v2.11.41 to v2.11.42
- #8466: @josegonzalez chore: bump dependencies in tests/apps/multi
Add `domains:clear-global`, `logs:vector-*`, and `scheduler-k3s`
cluster, profile, and system commands that take resource names
instead of app names as their first argument.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dokku client auto-detects the app name from the git remote
and prepends `--app <name>` to all SSH commands not explicitly
excluded. Network commands like `network:create` take a network
name (not an app name) as their first argument, so the injected
app name was being used as the network name instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GraphQL servers like Yoga v5 and Apollo Server 4 use the application/graphql-response+json content type per the GraphQL-over-HTTP spec (IANA registered). Without this in gzip_types, nginx silently skips compression for all GraphQL responses, sending megabytes of uncompressed JSON over the wire.
# History
## 0.37.7
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.7/bootstrap.sh
sudo DOKKU_TAG=v0.37.7 bash bootstrap.sh
```
### Bug Fixes
- #8379: @josegonzalez Call correct function for limiting letsencrypt to certain domains
### New Features
- #8405: @josegonzalez Add Debian 13 versions of docker plugin packages
- #8261: @josegonzalez Configure Traefik readiness healthchecks based on app.json config
- #8378: @farmdawgnation Support custom entrypoint for api and dashboard
### Dependencies
- #8395: @dependabot[bot] chore(deps): bump mvdan.cc/sh/v3 from 3.12.0 to 3.13.0 in /plugins/cron
- #8401: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.38 to 2.11.40 in /plugins/scheduler-k3s
- #8400: @dependabot[bot] chore(deps): bump github.com/melbahja/goph from 1.4.0 to 1.5.0 in /plugins/common
- #8394: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.47.0 to 0.48.0 in /plugins/common
- #8396: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.19.0 to 0.20.0 in /plugins/common
- #8399: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.19.0 to 0.20.0 in /plugins/scheduler-k3s
- #8398: @dependabot[bot] chore(deps): bump traefik from 3.6.9 to v3.6.10 in /plugins/traefik-vhosts
- #8397: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.19.0 to 0.20.0 in /plugins/scheduler-docker-local
- #8314: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.39.0 to 1.39.1 in /plugins/common
- #8380: @dependabot[bot] chore(deps): bump lucaslorentz/caddy-docker-proxy from 2.10 to 2.11 in /plugins/caddy-vhosts
- #8381: @dependabot[bot] chore(deps): bump k8s.io/kubectl from 0.35.0 to 0.35.2 in /plugins/scheduler-k3s
- #8382: @dependabot[bot] chore(deps): bump docker/login-action from 3 to 4
- #8383: @dependabot[bot] chore(deps): bump docker/setup-qemu-action from 3 to 4
- #8384: @dependabot[bot] chore(deps): bump mkdocs-material from 9.7.3 to 9.7.4 in /docs/_build
- #8385: @dependabot[bot] chore(deps): bump django from 5.2.11 to 5.2.12 in /tests/apps/dockerfile-release
- #8387: @dependabot[bot] chore(deps): bump docker/setup-buildx-action from 3 to 4
- #8388: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.79.1 to 1.79.2 in /tests/apps/gogrpc
- #8389: @dependabot[bot] chore(deps): bump golang from 1.26.0 to 1.26.1 in /tests/apps/go-fail-postdeploy
- #8390: @dependabot[bot] chore(deps): bump golang from 1.26.0 to 1.26.1 in /tests/apps/gogrpc
- #8391: @dependabot[bot] chore(deps): bump golang from 1.26.0 to 1.26.1 in /tests/apps/go-fail-predeploy
- #8392: @dependabot[bot] chore(deps): bump golang from 1.26.0 to 1.26.1 in /tests/apps/zombies-dockerfile-tini
- #8393: @dependabot[bot] chore(deps): bump golang from 1.26.0 to 1.26.1 in /tests/apps/zombies-dockerfile-no-tini
- #8354: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.1 in /tests/apps/gogrpc
- #8340: @dependabot[bot] chore(deps): bump python from 3.14.2-alpine to 3.14.3-alpine in /docs/_build
- #8353: @dependabot[bot] chore(deps): bump gunicorn from 25.0.2 to 25.1.0 in /tests/apps/multi
- #8357: @dependabot[bot] chore(deps): bump qs from 6.14.1 to 6.14.2 in /tests/apps/checks-root
- #8361: @dependabot[bot] chore(deps): bump flask from 3.1.2 to 3.1.3 in /tests/apps/multi
- #8360: @dependabot[bot] chore(deps): bump rack from 3.2.4 to 3.2.5 in /tests/apps/ruby
- #8332: @dependabot[bot] chore(deps-dev): bump org.apache.maven.plugins:maven-dependency-plugin from 3.9.0 to 3.10.0 in /tests/apps/java
- #8336: @dependabot[bot] chore(deps): bump python from 3.14.2-bookworm to 3.14.3-bookworm in /tests/apps/dockerfile-release
- #8355: @dependabot[bot] chore(deps): bump gunicorn from 25.0.2 to 25.1.0 in /tests/apps/python-flask
- #8356: @dependabot[bot] chore(deps): bump gunicorn from 25.0.2 to 25.1.0 in /tests/apps/dockerfile-release
- #8358: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.20.1 to 10.21 in /docs/_build
- #8371: @dependabot[bot] chore(deps): bump byjg/easy-haproxy from 5.0.0 to 6.0.1 in /plugins/haproxy-vhosts
- #8362: @dependabot[bot] chore(deps): bump flask from 3.1.2 to 3.1.3 in /tests/apps/python-flask
- #8374: @dependabot[bot] chore(deps): bump actions/download-artifact from 7 to 8
- #8375: @dependabot[bot] chore(deps): bump actions/upload-artifact from 6 to 7
- #8376: @dependabot[bot] chore(deps): bump phusion/baseimage from noble-1.0.2 to noble-1.0.3
- #8377: @dependabot[bot] chore(deps): bump whitenoise from 6.11.0 to 6.12.0 in /tests/apps/dockerfile-release
- #8365: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.36 to 2.11.37 in /plugins/scheduler-k3s
- #8367: @dependabot[bot] chore(deps): bump werkzeug from 3.1.5 to 3.1.6 in /tests/apps/python-flask
- #8368: @dependabot[bot] chore(deps): bump dj-database-url from 3.1.0 to 3.1.2 in /tests/apps/dockerfile-release
- #8372: @dependabot[bot] chore(deps): bump mkdocs-material from 9.7.1 to 9.7.3 in /docs/_build
- #8373: @dependabot[bot] chore(deps): bump traefik from 3.6.7 to 3.6.9 in /plugins/traefik-vhosts
- #8351: @dependabot[bot] chore(deps): bump golang from 1.25.6 to 1.26.0 in /tests/apps/zombies-dockerfile-tini
- #8350: @dependabot[bot] chore(deps): bump golang from 1.25.6 to 1.26.0 in /tests/apps/zombies-dockerfile-no-tini
- #8349: @dependabot[bot] chore(deps): bump golang from 1.25.6 to 1.26.0 in /tests/apps/go-fail-postdeploy
- #8348: @dependabot[bot] chore(deps): bump golang from 1.25.6 to 1.26.0 in /tests/apps/gogrpc
- #8347: @dependabot[bot] chore(deps): bump golang from 1.25.6 to 1.26.0 in /tests/apps/go-fail-predeploy
- #8344: @dependabot[bot] chore(deps): bump sigs.k8s.io/kustomize/api from 0.21.0 to 0.21.1 in /plugins/scheduler-k3s
- #8329: @dependabot[bot] chore(deps): bump gunicorn from 25.0.1 to 25.0.2 in /tests/apps/multi
- #8330: @dependabot[bot] chore(deps): bump gunicorn from 25.0.1 to 25.0.2 in /tests/apps/python-flask
- #8331: @dependabot[bot] chore(deps): bump gunicorn from 25.0.1 to 25.0.2 in /tests/apps/dockerfile-release
- #8320: @dependabot[bot] chore(deps): bump django from 5.2.10 to 5.2.11 in /tests/apps/dockerfile-release
- #8321: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.35 to 2.11.36 in /plugins/scheduler-k3s
- #8319: @dependabot[bot] chore(deps): bump gunicorn from 24.1.1 to 25.0.1 in /tests/apps/dockerfile-release
- #8318: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.19.2 to 1.19.3 in /plugins/scheduler-k3s
- #8317: @dependabot[bot] chore(deps): bump gunicorn from 24.1.1 to 25.0.1 in /tests/apps/multi
- #8316: @dependabot[bot] chore(deps): bump gunicorn from 24.1.1 to 25.0.1 in /tests/apps/python-flask
Add two new global-only properties to configure a dedicated Traefik entrypoint
for the API and dashboard, enabling network isolation on private interfaces.
Includes comprehensive test coverage for property management and compose config.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
# History
## 0.37.6
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.6/bootstrap.sh
sudo DOKKU_TAG=v0.37.6 bash bootstrap.sh
```
### Bug Fixes
- #8306: @josegonzalez Ensure we can call storage:report properly
### Documentation
- #8289: @josegonzalez Add an architecture document for dokku
- #8288: @josegonzalez Add a readme for the docs folder
### Dependencies
- #8312: @dependabot[bot] chore(deps): bump timberio/vector from 0.52.0-debian to 0.53.0-debian in /plugins/logs
- #8311: @dependabot[bot] chore(deps): bump gunicorn from 24.0.0 to 24.1.1 in /tests/apps/dockerfile-release
- #8308: @dependabot[bot] chore(deps): bump gunicorn from 24.0.0 to 24.1.1 in /tests/apps/multi
- #8309: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.20 to 10.20.1 in /docs/_build
- #8307: @dependabot[bot] chore(deps): bump gunicorn from 24.0.0 to 24.1.1 in /tests/apps/python-flask
- #8301: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.19.5 to 3.20.0 in /plugins/scheduler-k3s
- #8299: @dependabot[bot] chore(deps): bump ruby from 4.0.0 to 4.0.1 in /tests/apps/dockerfile-entrypoint
- #8293: @dependabot[bot] chore(deps): bump golang from 1.25.5 to 1.25.6 in /tests/apps/zombies-dockerfile-no-tini
- #8296: @dependabot[bot] chore(deps): bump soupsieve from 2.8.1 to 2.8.3 in /docs/_build
- #8295: @dependabot[bot] chore(deps): bump golang from 1.25.5 to 1.25.6 in /tests/apps/go-fail-postdeploy
- #8294: @dependabot[bot] chore(deps): bump golang from 1.25.5 to 1.25.6 in /tests/apps/gogrpc
- #8291: @dependabot[bot] chore(deps): bump golang from 1.25.5 to 1.25.6 in /tests/apps/zombies-dockerfile-tini
- #8292: @dependabot[bot] chore(deps): bump golang from 1.25.5 to 1.25.6 in /tests/apps/go-fail-predeploy
- #8277: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.34 to 2.11.35 in /plugins/scheduler-k3s
- #8297: @dependabot[bot] chore(deps): bump pyparsing from 3.3.1 to 3.3.2 in /docs/_build
- #8298: @dependabot[bot] chore(deps): bump lodash from 4.17.21 to 4.17.23 in /tests/apps/multi
- #8300: @dependabot[bot] chore(deps): bump packaging from 25.0 to 26.0 in /docs/_build
- #8303: @dependabot[bot] chore(deps): bump gunicorn from 23.0.0 to 24.0.0 in /tests/apps/python-flask
- #8305: @dependabot[bot] chore(deps): bump gunicorn from 23.0.0 to 24.0.0 in /tests/apps/dockerfile-release
- #8304: @dependabot[bot] chore(deps): bump gunicorn from 23.0.0 to 24.0.0 in /tests/apps/multi
- #8280: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.19.4 to 3.19.5 in /plugins/scheduler-k3s
- #8279: @dependabot[bot] chore(deps): bump traefik from 3.6.6 to 3.6.7 in /plugins/traefik-vhosts
# History
## 0.37.5
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.5/bootstrap.sh
sudo DOKKU_TAG=v0.37.5 bash bootstrap.sh
```
### Bug Fixes
- #8274: @josegonzalez Delete the tls app chart when the app is deleted
- #8273: @josegonzalez Ensure the destination directory exists when extracting files from a repository
- #8263: @josegonzalez Set correct version for builder-railpack plugin
### New Features
- #8268: @josegonzalez Add the ability to log into a registry on a per-app basis
- #8258: @josegonzalez Add support for dns-01 challenge mode when using traefik
- #8262: @josegonzalez Use certificates imported by certs plugin when deploying via scheduler-k3s
- #8266: @josegonzalez Add a method to force build when re-using an image with git:from-image
- #8265: @josegonzalez Add the ability to skip setting the deploy-branch when running git:sync
### Refactors
- #8264: @josegonzalez Rewrite the storage plugin in golang
### Dependencies
- #8270: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.38.3 to 1.39.0 in /plugins/common
- #8267: @dependabot[bot] chore(deps): bump werkzeug from 3.1.4 to 3.1.5 in /tests/apps/python-flask
- #8260: @dependabot[bot] chore(deps): bump django from 5.2.9 to 5.2.10 in /tests/apps/dockerfile-release
- #8256: @dependabot[bot] chore(deps): bump luizm/action-sh-checker from 0.9.0 to 0.10.0
- #8257: @dependabot[bot] chore(deps): bump dj-database-url from 3.0.1 to 3.1.0 in /tests/apps/dockerfile-release
# History
## 0.37.4
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.4/bootstrap.sh
sudo DOKKU_TAG=v0.37.4 bash bootstrap.sh
```
### Bug Fixes
- #8168: @josegonzalez Install logrotate in the container
### Tests
- #8254: @josegonzalez Add a test to prove cron:run commands work as expected
- #8255: @josegonzalez Update test ruby app
### Dependencies
- #8252: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.19.1 to 10.20 in /docs/_build
- #8253: @dependabot[bot] chore(deps): bump qs from 6.14.0 to 6.14.1 in /tests/apps/checks-root
- #8249: @dependabot[bot] chore(deps): bump traefik from 3.6.5 to 3.6.6 in /plugins/traefik-vhosts
- #8250: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.33 to 2.11.34 in /plugins/scheduler-k3s
- #8247: @dependabot[bot] chore(deps): bump ruby from 3.4.8 to 4.0.0 in /tests/apps/dockerfile-entrypoint
### Other
- #8248: @osbre Add `proxy-keepalive` to Nginx properties
# History
## 0.37.3
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.37.3/bootstrap.sh
sudo DOKKU_TAG=v0.37.3 bash bootstrap.sh
```
### Bug Fixes
- #8199: @josegonzalez Pass global build args as array when building nixpacks apps
- #8236: @josegonzalez Ensure the /etc/docker exists when interacting with it via postinst
### Documentation
- #8229: @kleutzinger Fix command syntax for removing docker options
- #8219: @deanmarano Add deanmarano to community plugins list
- #8205: @deanmarano Add DNS plugin to community plugins list
### Tests
- #8246: @josegonzalez Add tests to prove that Procfile tasks all work for every builder
### Dependencies
- #8243: @dependabot[bot] chore(deps): bump k8s.io/kubectl from 0.34.2 to 0.35.0 in /plugins/scheduler-k3s
- #8245: @dependabot[bot] chore(deps): bump github.com/go-resty/resty/v2 from 2.17.0 to 2.17.1 in /plugins/scheduler-k3s
- #8244: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.34.2 to 1.35.0 in /plugins/scheduler-k3s
- #8233: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.38.2 to 1.38.3 in /plugins/common
- #8241: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.32 to 2.11.33 in /plugins/scheduler-k3s
- #8240: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.19.1 to 1.19.2 in /plugins/scheduler-k3s
- #8239: @dependabot[bot] chore(deps): bump pyparsing from 3.2.5 to 3.3.1 in /docs/_build
- #8238: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.77.0 to 1.78.0 in /tests/apps/gogrpc
- #8237: @dependabot[bot] chore(deps): bump github.com/kedacore/keda/v2 from 2.18.2 to 2.18.3 in /plugins/scheduler-k3s
- #8235: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.19.2 to 3.19.4 in /plugins/scheduler-k3s
- #8232: @dependabot[bot] chore(deps): bump importlib-metadata from 8.7.0 to 8.7.1 in /docs/_build
- #8234: @dependabot[bot] chore(deps): bump k8s.io/client-go from 0.34.2 to 0.35.0 in /plugins/scheduler-k3s
- #8231: @dependabot[bot] chore(deps): bump github.com/expr-lang/expr from 1.17.6 to 1.17.7 in /plugins/scheduler-k3s
- #8208: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.45.0 to 0.46.0 in /plugins/common
- #8201: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.18.0 to 0.19.0 in /plugins/common
- #8214: @dependabot[bot] chore(deps): bump google.golang.org/protobuf from 1.36.10 to 1.36.11 in /tests/apps/gogrpc
- #8216: @dependabot[bot] chore(deps): bump actions/download-artifact from 6 to 7
- #8217: @dependabot[bot] chore(deps): bump actions/upload-artifact from 5 to 6
- #8228: @dependabot[bot] chore(deps): bump soupsieve from 2.8 to 2.8.1 in /docs/_build
- #8226: @dependabot[bot] chore(deps): bump ruby from 3.4.7 to 3.4.8 in /tests/apps/dockerfile-entrypoint
- #8227: @dependabot[bot] chore(deps): bump mkdocs-material from 9.7.0 to 9.7.1 in /docs/_build
- #8224: @dependabot[bot] chore(deps): bump traefik from 3.6.4 to 3.6.5 in /plugins/traefik-vhosts
- #8225: @dependabot[bot] chore(deps): bump timberio/vector from 0.51.1-debian to 0.52.0-debian in /plugins/logs
- #8220: @dependabot[bot] chore(deps): bump python from 3.14.1-bookworm to 3.14.2-bookworm in /tests/apps/dockerfile-release
- #8221: @dependabot[bot] chore(deps): bump python from 3.14.1-alpine to 3.14.2-alpine in /docs/_build
- #8218: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.19 to 10.19.1 in /docs/_build
- #8215: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.18 to 10.19 in /docs/_build
- #8209: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.38.2 to 1.38.3 in /plugins/config
- #8210: @dependabot[bot] chore(deps): bump github.com/kedacore/keda/v2 from 2.18.1 to 2.18.2 in /plugins/scheduler-k3s
- #8211: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.22.3 to 0.22.4 in /plugins/scheduler-k3s
- #8200: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.17.2 to 10.18 in /docs/_build
- #8202: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.18.0 to 0.19.0 in /plugins/scheduler-docker-local
- #8203: @dependabot[bot] chore(deps): bump traefik from 3.6.2 to 3.6.4 in /plugins/traefik-vhosts
- #8204: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.18.0 to 0.19.0 in /plugins/scheduler-k3s
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}."
Every deploy that flows through Dokku - whether triggered by `git push`, `ps:rebuild`, `ps:restart`, `config:set`, or `git:from-archive` / `git:from-image` / `git:sync` / `git:load-image` - is recorded as a structured build record on disk. The `builds` plugin lets operators inspect what is currently deploying, look up the result of an old deploy, and stream the captured build log without depending on `journalctl`.
```
builds:cancel <app> # Cancel a running build for an app
builds:info <app> <build-id> [--format json] # Show details for a single build
# List builds (running across all apps, or running + history for one)
builds:output <app> [<build-id>|current] # Show build output (tail for live, cat for finished)
builds:prune <app> [--all-apps] # Reap abandoned records and apply retention
builds:report [<app>] [<flag>] # Display a build report
builds:set [--global|<app>] <key> [<value>] # Set or clear a builds property
```
## Build records
Every build is identified by a sortable base36 ULID-style id (`DOKKU_BUILD_ID`) generated at deploy start. For each build, Dokku writes:
-`$DOKKU_LIB_ROOT/data/builds/<app>/<build-id>.json` - the structured record
-`$DOKKU_LIB_ROOT/data/builds/<app>/<build-id>.log` - the captured stdout/stderr of the deploy
Output is also tagged into syslog as `dokku-<build-id>` so `journalctl -t dokku-<build-id>` continues to work. The on-disk log file is the durable source of truth and is read for `builds:output` even when journald has rotated old entries away.
### Record schema
```json
{
"id":"01j8c4xv7bk5w3",
"app":"myapp",
"kind":"build",
"pid":12345,
"started_at":"2026-04-30T13:50:00Z",
"finished_at":"2026-04-30T13:51:14Z",
"status":"succeeded",
"source":"git-hook",
"exit_code":0
}
```
- **kind** - `build` for paths that produce a new image (`git push`, `git:*`, `ps:rebuild`); `deploy` for paths that re-deploy an existing image (`ps:restart`, `ps:start`, `dokku deploy`, `config:set`).
- **status** - on-disk values are `running | succeeded | failed | canceled`. `abandoned` is a fifth display-only value computed at read time for `running` records whose PID is no longer alive; it is never persisted to the record.
- **source** - the user-typed command that originated the deploy (e.g. `git-hook`, `ps:restart`, `config-redeploy`, `git:sync`).
## Listing builds
Without an app argument, `builds:list` shows currently-running builds across every app on the host:
# tail -f the live build, or cat the log for a finished one
dokku builds:output myapp 01j8c4xv7bk5w3
# resolve "current" from the in-flight deploy
dokku builds:output myapp current
```
If the on-disk log file is missing (for example, a build from before this plugin was installed), `builds:output` falls back to `journalctl -t dokku-<build-id>`.
## Cancelling a build
`builds:cancel` reads the active `.deploy.lock` for an app, looks up the matching build record, and sends `SIGQUIT` to the deploy's process group. The record is finalized as `canceled` (or `failed` if the process had already exited without finalizing - in that case Dokku marks the record so it doesn't sit in `running` forever).
```shell
dokku builds:cancel myapp
```
If the record is already finalized when cancel runs, no signal is sent and the record is left untouched.
## Retention
Build records are pruned by count, not by age. The default retention is **20** records per app. Live in-flight deploys are never pruned regardless of count.
Set a per-app retention:
```shell
dokku builds:set myapp retention 50
```
Set the global default:
```shell
dokku builds:set --global retention 10
```
Clear an override (falls back to the global value, then the default):
```shell
dokku builds:set myapp retention
```
## Manual pruning
`builds:prune` invokes the same logic that runs at the end of every deploy - it reaps abandoned records (status=running with a dead PID, finalized as `failed`) and trims the directory to the configured retention. Useful after lowering retention via `builds:set` or to clean up after a host reboot:
```shell
dokku builds:prune myapp
dokku builds:prune --all-apps
```
## Reports
```shell
dokku builds:report
dokku builds:report myapp
dokku builds:report myapp --build-status
```
Available flags:
-`--build-id`, `--build-kind`, `--build-status`, `--build-pid`, `--build-source`, `--build-started-at`, `--build-finished-at`, `--build-exit-code`: details of the most recent build for the app
-`--builds-retention`: per-app retention override (empty if none)
-`--builds-global-retention`: global retention override (empty if none)
-`--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.
docker-options:list <app> [--process PROC] --phase PHASE # List docker options for one process+phase pair
docker-options:remove [--process PROC...] <app> <phase(s)> OPTION # Remove Docker option from app for phase
docker-options:report [<app>] [<flag>] [--format json|stdout] # Displays a docker options report for one or more apps
```
The `docker-options` plugin allows users to specify custom [container options](https://docs.docker.com/engine/reference/run/) for containers created by Dokku at various phases.
@@ -57,11 +58,17 @@ Multiple phases can be specified by using a comma when specifying phases:
The `docker-options:add` does not support setting multiple options in a single call. To specify multiple options, call `docker-options:add` multiple times.
Multiple dockeroptions can also be specified in a single call. Each `--flag [value]` group is detected on flag boundaries, shell-tokenized for quoting safety, and stored as its own entry so it round-trips through `docker-options:report` and `docker-options:list`:
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:
```shell
dokku docker-options:add --process web node-js-app deploy "--ulimit nofile=12""--shm-size 256m"
dokku docker-options:add node-js-app deploy "--ulimit nofile=12""--shm-size 256m" --process web
To remove docker options from an app, use the `docker-options:remove` command. This takes an app name, a comma-separated list of phases, and the docker-option to remove.
```shell
dokku docker-options:remove node-js-app run ""--ulimit nofile=12"
dokku docker-options:remove node-js-app run "--ulimit nofile=12"
```
Multiple phases can be specified by using a comma when specifying phases:
@@ -78,11 +85,10 @@ Multiple phases can be specified by using a comma when specifying phases:
The `docker-options:remove` does not support setting multiple options in a single call. To specify multiple options, call `docker-options:remove` multiple times.
Multiple dockeroptions can also be removed in a single call, mirroring the splitting that `docker-options:add` performs:
When process-specific options are configured (see below), the report exposes one additional dynamic flag per configured `process.deploy` pair, named `--docker-options-deploy.<process>`:
Docker options can be scoped to specific process types declared in the app's `Procfile` by passing one or more `--process` flags. This is useful when a deploy-phase option (for example a port mapping) makes sense for one process type but would conflict with another - the canonical case being a `web` process that needs `-p 6789:5000` published while the `worker` process must not bind that port.
Process scoping is supported only for the `deploy` phase. The `build` phase runs once per app and the `run` phase covers ad-hoc commands and cron tasks where no Procfile process type is in play; both reject `--process`.
There is no `--global` flag. Omitting `--process` keeps the historical behavior of applying the option to every container in the app. Avoiding a `--global` flag here is intentional: elsewhere in Dokku `--global` means "across all apps" (e.g. `dokku config:set --global`), which would be misleading in this plugin where the scope is always one app.
#### Setting process-specific options
```shell
# Add a port mapping only to the web process
dokku docker-options:add --process web node-js-app deploy "-p 6789:5000"
Multiple `--process` flags can be combined to apply the same option to several process types in one call:
```shell
dokku docker-options:add --process web --process api node-js-app deploy "-v /shared:/shared"
```
If `--process` names a process type that is not currently declared in the app's `Procfile`, the command succeeds but emits a warning. This allows configuring options ahead of a deploy that adds the new process type.
The `_default_` value is reserved internally and cannot be passed to `--process`.
#### Removing and clearing process-specific options
```shell
# Remove a single option from one process
dokku docker-options:remove --process web node-js-app deploy "-p 6789:5000"
Without `--process`, `:remove` and `:clear` operate on the default scope only - per-process lists are left untouched.
#### Listing options for a process and phase
The `docker-options:list` command prints the options stored for a single process+phase pair, one option per line. Omitting `--process` lists the default scope.
```shell
dokku docker-options:list node-js-app --process web --phase deploy
> New as of 0.5.0. Named storage entries new as of 0.38.0.
The preferred method to mount external containers to a Dokkumanaged container, is to use the Dokku storage plugin.
The preferred method to attach persistent storage to a Dokku-managed container is the Dokku storage plugin.
```
storage:ensure-directory [--chown option] <directory> # Creates a persistent storage directory in the recommended storage path
storage:list <app> [--format text|json] # List bind mounts for app's container(s) (host:container)
storage:mount <app> <host-dir:container-dir> # Create a new bind mount
storage:report [<app>] [<flag>] # Displays a checks report for one or more apps
storage:unmount <app> <host-dir:container-dir> # Remove an existing bind mount
storage:create <name> [<path>] [flags] # Register a named storage entry
storage:destroy <name> # Remove a named storage entry (must be unmounted from every app first)
storage:ensure-directory [--chown option] <directory> # [DEPRECATED] use storage:create instead
storage:exec <name> [-- <cmd>...] # Run a command (or shell) in a temporary container that mounts the entry
storage:info <name> [--format text|json] # Show details for one storage entry
storage:list <app> [--format text|json] # List bind mounts for an app's container(s) (legacy host:container view)
storage:list-entries [--scheduler s] [--format text|json] # List registered storage entries
storage:mount <app> <name> --container-dir <path> [flags] # Mount a named entry into an app
storage:mount <app> <host-dir:container-dir> # [LEGACY] colon-form mount, docker-local only
storage:report [<app>] [<flag>] # Display a storage report for one or more apps
storage:report --global # Display a cluster-wide entry inventory
storage:set <name> [flags] # Update a storage entry in place
storage:unmount <app> <name> [--container-dir <path>] # Remove an attachment
storage:wait <name> # Block until a k3s entry's PVC is bound
```
> The storage plugin is compatible with storage mounts created with the docker-options. The storage plugin will only list mounts from the deploy/run phase.
A storage entry is the source of truth for the underlying volume - a host directory on docker-local, or a PersistentVolumeClaim on k3s. Multiple apps can mount the same entry, and an attachment carries the per-app details (container path, phases, subpath, readonly, process type). Names are globally unique across the install and must be DNS-1123 labels (lowercase letters, digits, dashes) of 45 characters or less so they can be used verbatim as Helm release and PVC names.
The legacy `storage:mount <app> <host>:<container>` form continues to work on docker-local. On a k3s app it is rejected; create a named entry with `storage:create --scheduler k3s` and mount it instead. Existing colon-form mounts are migrated automatically the first time the new code runs - they show up as `legacy-<hash>` entries in `storage:list-entries`.
The storage plugin supports the following mount points:
- explicit paths that exist on the host
- docker volumes
- explicit paths that exist on the host (docker-local)
- docker volumes (docker-local)
- PersistentVolumeClaims provisioned via a StorageClass (k3s)
registry:login [--password-stdin] <server> <username> [<password>] # Login to a docker registry
registry:report [<app>] [<flag>]# Displays a registry report for one or more apps
registry:set <app> <key> (<value>) # Set or clear a registry property for an app
registry:login [--global|--password-stdin] [<app>] <server> <username> [<password>] # Login to a docker registry
registry:logout [--global] [<app>] <server> # Logout from a docker registry
registry:report [<app>] [<flag>] # Displays a registry report for one or more apps
registry:set <app>|--global <key> (<value>) # Set or clear a registry property for an app
```
The registry plugin enables interacting with remote registries, which is useful when either deploying images via `git:from-image` or when interacting with custom schedulers to deploy built image artifacts.
@@ -15,34 +16,75 @@ The registry plugin enables interacting with remote registries, which is useful
### Logging into a registry
The `registry:login` command can be used to log into a docker registry. The following are examples for logging into various common registries:
The `registry:login` command can be used to log into a docker registry. Credentials can be stored globally (for all apps) or on a per-app basis.
#### Global login
To log in globally (credentials shared by all apps), use the `--global` flag:
For security reasons, the password may also be specified as stdin by specifying the `--password-stdin` flag. This is supported regardless of the registry being logged into.
> [!NOTE]
> For backwards compatibility, if the `--global` flag is omitted and only three arguments are provided (server, username, password), the command will behave as a global login but will show a deprecation warning.
#### Per-app login
To log in for a specific app, specify the app name as the first argument:
Per-app credentials are stored in `/var/lib/dokku/config/registry/$APP/config.json` and are automatically used for docker operations (build, push, pull) for that specific app.
#### Password via stdin
For security reasons, the password may also be specified as stdin by specifying the `--password-stdin` flag. This is supported for both global and per-app logins:
For certain Docker registries - such as Amazon ECR or Google's GCR registries - users may instead wish to use a docker credential helper to automatically authenticate against a server; please see the documentation regarding the credential helper in question for further setup instructions.
### Logging out from a registry
The `registry:logout` command can be used to log out from a docker registry:
```shell
# global logout
dokku registry:logout --global docker.io
# per-app logout
dokku registry:logout node-js-app docker.io
```
When an app is destroyed, any per-app registry credentials are automatically removed.
### Setting a remote server
To specify a remote server registry for pushes, set the `server` property via the `registry:set` command. The default value for this property is empty string. Setting the value to `docker.io` or `hub.docker.com` will result in the computed value being empty string (as that is the default, implicit registry), while any non-zero length value will have a `/` appended to it if there is not one already.
See the [Supported Resource Management Properties](/docs/deployment/schedulers/docker-local.md#supported-resource-management-properties) section of the docker local scheduler documentation for more information on how each resource limit maps to Docker.
Resource limits and reservations are applied only during the `run` and `deploy` phases of an application, and will not impact the `build` phase of an application.
Resource reservations are applied only during the `run` and `deploy` phases of an application. Resource limits also apply during the `build` phase when explicitly configured against the `build` process type - see [Build-time Resource Limits](#build-time-resource-limits) below.
### Resource Limits
@@ -120,6 +120,40 @@ dokku resource:limit --process-type web node-js-app
nvidia-gpu:
```
#### Build-time Resource Limits
> [!IMPORTANT]
> New as of 0.38.0
Resource limits may be applied to the build container by using the special `build` process type. This allows constraining memory and CPU usage during the `build` phase, which is otherwise unconstrained.
=====> Setting resource limits for node-js-app (build)
memory: 4g
```
Build-time limits are applied via the `docker-args-process-build` plugin trigger. They do not inherit from default (`_default_`) limits - only limits explicitly set against the `build` process type are applied at build time. This is intentional: build phases often require more memory than the runtime process, and silently inheriting a small runtime limit would cause confusing OOM failures.
> [!WARNING]
> Do not use `build` as a runtime process type in your `Procfile`. The `build` name is reserved by the resource plugin to scope limits to the build container, and using it as a Procfile entry will cause those limits to be applied to that runtime container as well.
Resource keys outside the supported set for a builder are silently ignored. Reservations (`resource:reserve`) are never applied at build time - only limits.
#### Clearing Resource Limits
In cases where the values are incorrect - or there is no desire to limit resources - resource limits may be cleared using the `resource:limit-clear` command.
- Dokku now generates a minimal nginx configuration for apps without running `web` processes (undeployed apps, apps with no `web` process type, or apps with stopped web processes). This configuration returns `502 Bad Gateway` responses, ensuring the app's domain resolves and monitoring tools can detect non-200 status codes. The configuration is automatically replaced with the full proxy configuration once the app is deployed with running `web` processes. See the [nginx documentation](/docs/networking/proxies/nginx.md#nginx-configuration-for-undeployed-apps) for more details.
- Users with custom `nginx.conf.sigil` templates that reference `DOKKU_APP_WEB_LISTENERS` should be aware that this variable may now be empty when the template is rendered for apps without running web processes. Custom templates should handle this case gracefully, for example by using a conditional to serve an error page instead of proxying:
- 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.
- 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. 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.
- 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.
- 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 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
A number of `DOKKU_*` config environment variables have been replaced with properly-namespaced plugin properties. Existing values are migrated automatically the first time `dokku` runs after the upgrade, and the original config variable is unset. No manual action is required.
Going forward, configure these settings using the property commands listed below. Setting the deprecated env vars via `dokku config:set` will no longer have any effect.
| Deprecated Env Var | Replacement Command |
|---|---|
| `DOKKU_APP_PROXY_TYPE` | `dokku proxy:set <app> type <value>` |
`DOKKU_PARALLEL_ARGUMENTS` is removed entirely; it has no replacement.
`DOKKU_SKIP_CLEANUP` continues to be honored when set in `/etc/environment` or `~dokku/.dokkurc/*` so that bootstrap-time configuration keeps working, but the `builder skip-cleanup` property is the canonical interface and takes precedence when set.
> While the `app.json` format used by Dokku is based on the one [supported by Heroku](https://devcenter.heroku.com/articles/app-json-schema), not all Heroku functionality is supported by Dokku.
(list, optional) A list of buildpacks to use when deploying the app. Each entry is an object with a `url` property. Buildpacks specified via the `buildpacks:add` or `buildpacks:set` commands take precedence over those specified in `app.json`. Buildpack URLs may use the shorthand format (e.g., `heroku/python`) which will be expanded to the full GitHub URL.
-`url`: (string, required) The URL or shorthand reference of the buildpack.
"description":"A variable that updates on every deploy",
"value":"synced_value",
"sync":true
}
}
}
```
(object, optional) A key-value object for environment variable configuration. Keys are the variable names. Values can be either a string (used as the default value) or an object with the following properties:
-`description`: (string, optional) Human-readable explanation of the variable's purpose
-`value`: (string, optional) Default value for the variable
-`required`: (boolean, optional, default: `true`) Whether the variable must have a value
-`generator`: (string, optional) Function to generate the value. Currently only `"secret"` is supported, which generates a 64-character cryptographically secure hex string
-`sync`: (boolean, optional, default: `false`) If `true`, the value will be set on every deploy, overwriting any existing value
### Behavior
Environment variables from `app.json` are processed during the first deploy, before the predeploy script runs. The behavior depends on the variable configuration:
1.**Variables with `value` or simple string**: The default value is set if the variable doesn't already exist
2.**Variables with `generator: "secret"`**: A random 64-character hex string is generated if the variable doesn't exist
3.**Required variables without a value or generator**: If a TTY is available, the user is prompted for a value. Otherwise, the deploy fails with an error
4.**Optional variables without a value**: Skipped silently if no TTY is available
On subsequent deploys:
- Variables are NOT re-set unless `sync: true` is specified
- Variables with `sync: true` are always set to their configured value, overwriting any manual changes
- Variables that already have values are not modified
### Examples
**Simple default value:**
```json
{
"env":{
"WEB_CONCURRENCY":"5"
}
}
```
**Generated secret (recommended for API keys, tokens, etc.):**
```json
{
"env":{
"SECRET_KEY_BASE":{
"description":"Base secret for session encryption",
The `nginx.conf.sigil` file is used to configure the nginx server for an application. The default template can be found [here](https://github.com/dokku/dokku/blob/master/plugins/nginx-vhosts/templates/nginx.conf.sigil). Dokku uses a tool named [sigil](https://github.com/gliderlabs/sigil) to generate the nginx configuration based on the template provided.
## Validation
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.
@@ -48,7 +48,7 @@ Vhosts can also be disabled for all apps:
dokku domains:disable --all
```
On subsequent deploys, the nginx virtualhost will be discarded. This is useful when deploying internal-facing services that should not be publicly routeable. As of 0.4.0, nginx will still be configured to proxy your app on some random high port. This allows internal services to maintain the same port between deployments. You may change this port by setting `DOKKU_PROXY_PORT` and/or `DOKKU_PROXY_SSL_PORT` (for services configured to use SSL.)
On subsequent deploys, the nginx virtualhost will be discarded. This is useful when deploying internal-facing services that should not be publicly routeable. As of 0.4.0, nginx will still be configured to proxy your app on some random high port. This allows internal services to maintain the same port between deployments. The non-SSL and SSL ports used can be customized via `dokku proxy:set <app> proxy-port <value>` and `dokku proxy:set <app> proxy-ssl-port <value>` - see the [proxy management documentation](/docs/networking/proxy-management.md#setting-proxy-ports) for details.
To re-enable, run the `domains:enable` subcommand:
@@ -22,12 +22,12 @@ Environment variables are available both at run time and during the application
For buildpack deploys, Dokku will create a `/app/.env` file that can be used for legacy buildpacks. Note that this is _not_ updated when `config:set` or `config:unset` is called, and is only written during a `deploy` or `ps:rebuild`. Developers are encouraged to instead read from the application environment directly, as the proper values will be available then.
> [!NOTE]
> Global `ENV` files are sourced before app-specific `ENV` files. This means that app-specific variables will take precedence over global variables. Configuring your global `ENV` file is manual, and should be considered potentially dangerous as configuration applies to all applications.
> Global environment variables are sourced before app-specific environment variables. This means that app-specific variables will take precedence over global variables. Configuring global environment variables should be considered potentially dangerous as configuration applies to all applications.
You can set multiple environment variables at once:
Whitespace and special characters get tricky. If you are using dokku locally you don't need to do any special escaping. If you are using dokku over ssh you will need to backslash-escape spaces:
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:
Environment variables can also be declared in an `app.json` file in your repository root. This is useful for setting default values, generating secrets, or requiring certain variables to be set before deployment.
```json
{
"env":{
"SIMPLE_VAR":"default_value",
"SECRET_KEY":{
"description":"A secret key for signing tokens",
"generator":"secret"
},
"DATABASE_URL":{
"description":"PostgreSQL connection string",
"required":true
}
}
}
```
Environment variables from `app.json` are processed during the first deploy, before the predeploy script runs. Variables can be configured as:
- **Simple string values**: Set as defaults if not already configured
- **Generated secrets**: Use `"generator": "secret"` to auto-generate a 64-character hex string
- **Required variables**: Use `"required": true` to prompt for or require a value
- **Synced variables**: Use `"sync": true` to update the value on every deploy
For full details on the `env` schema and behavior, see the [app.json documentation](/docs/appendices/file-formats/app-json.md#env).
## Special Config Variables
The following config variables have special meanings and can be set in a variety of ways. Unless specified via global app config, the values may not be passed into applications. Usage of these values within applications should be considered unsafe, as they are an internal configuration values that may be moved to the internal properties system in the future.
@@ -98,26 +127,5 @@ The following config variables have special meanings and can be set in a variety
| `DOKKU_QUIET_OUTPUT` | none | `--quiet` flag | Silences certain header output for `dokku` commands. |
| `DOKKU_RM_CONTAINER` | none | `dokku config:set` <br /> | Deprecated: Whether to keep `dokku run` containers around or not. |
| `DOKKU_TRACE` | none | `dokku trace:on` <br /> `dokku trace:off` <br /> `--trace` flag | Turn on very verbose debugging. |
| `DOKKU_APP_SHELL` | `/bin/bash` | `dokku config:set` | Allows users to change the default shell used by Dokku for `dokku enter` and execution of deployment tasks. |
| `DOKKU_CHECKS_WAIT` | `5` | `dokku config:set` | Wait this many seconds for the container to start before running checks.
| `DOKKU_CHECKS_TIMEOUT` | `30` | `dokku config:set` | Wait this many seconds for each response before marking it as a failure.
| `DOKKU_CHECKS_ATTEMPTS` | `5` | `dokku config:set` | Number of retries for to run for a specific check before marking it as a failure
| `DOKKU_DEFAULT_CHECKS_WAIT` | `10` | `dokku config:set` | If no user-defined checks are specified - or if the process being checked is not a `web` process - this is the period of time Dokku will wait before checking that a container is still running. |
| `DOKKU_DISABLE_PROXY` | none | `dokku proxy:disable` <br /> `dokku proxy:enable` | Disables the proxy in front of your application, resulting in publicly routing the docker container. |
| `DOKKU_DISABLE_APP_AUTOCREATION` | none | `dokku config:set` | Disables automatic creation of a non-existent app on deploy. |
| `DOKKU_SKIP_CLEANUP` | | `/etc/environment` <br /> `~dokku/.dokkurc` <br /> `~dokku/.dokkurc/*` | When a deploy is triggered, if this is set to a non-empty value, then old docker containers and images will not be removed. |
> 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).
@@ -13,6 +13,7 @@ apps:lock <app> # Locks an app for deployment
apps:locked <app> # Checks if an app is locked for deployment
apps:rename <old-app> <new-app> # Rename an app
apps:report [<app>] [<flag>] # Display report about an app
apps:set [--global] <app> <key> (<value>) # Set or clear an apps property for an app
apps:unlock <app> # Unlocks an app for deployment
```
@@ -78,6 +79,20 @@ Once created, you can configure the application as normal, and deploy the applic
- Create and link datastores.
- Set environment variables.
### Disabling automatic app creation
By default, pushing to a git remote for an app that does not yet exist on the Dokku host will cause the app to be created automatically. On shared or production hosts this may be undesirable - operators may prefer that an explicit `apps:create` is required first. The `disable-autocreation` global property controls this behavior:
```shell
dokku apps:set --global disable-autocreation true
```
While set, pushes targeting an app that does not exist will be rejected. The default behavior may be restored by passing an empty value:
```shell
dokku apps:set --global disable-autocreation
```
### Removing a deployed app
In some cases, you may need to destroy an application, whether it is because the application is temporary or because it was misconfigured. In these cases, you can use the `apps:destroy` command. Performing any destructive actions in Dokku requires confirmation, and this command will ask for the name of the application being deleted before doing so.
@@ -82,6 +82,28 @@ The default value may be set by passing an empty value for the option.
dokku builder:set --global build-dir
```
#### Skipping container and image cleanup
After a successful deploy, Dokku removes old containers and images that are no longer referenced by the app. On hosts where image rebuilds are expensive or where operators want to retain prior images for manual rollback, this cleanup can be disabled per-app or globally via the `skip-cleanup` property:
```shell
dokku builder:set node-js-app skip-cleanup true
```
The property can also be set globally:
```shell
dokku builder:set --global skip-cleanup true
```
The default behavior (cleanup enabled) may be restored by passing an empty value:
```shell
dokku builder:set node-js-app skip-cleanup
```
For backwards compatibility with bootstrap-time configuration, the `DOKKU_SKIP_CLEANUP` environment variable in `/etc/environment` or `~dokku/.dokkurc/*` is still honored when the `skip-cleanup` property is unset. The property is the canonical interface and takes precedence when set.
### Displaying builder reports for an app
You can get a report about the app's builder status using the `builder:report` command:
Buildpacks can also be specified in the `app.json` file at the root of your repository:
```json
{
"buildpacks":[
{
"url":"heroku/python"
}
]
}
```
Buildpacks configured via `buildpacks:add` or `buildpacks:set` commands take precedence over those specified in `app.json`. If no buildpacks are configured via commands and `app.json` contains a `buildpacks` entry, those buildpacks will be used.
Shorthand buildpack references (e.g., `heroku/python`) are expanded to full GitHub URLs automatically.
### Displaying buildpack reports for an app
You can get a report about the app's buildpacks status using the `buildpacks:report` command:
@@ -185,7 +185,7 @@ You would adjust the cache directory for whatever application cache you have, e.
### Customizing the run command
By default no arguments are passed to `docker run` when deploying the container and the `CMD` or `ENTRYPOINT` defined in the `Dockerfile` are executed. You can take advantage of docker ability of overriding the `CMD` or passing parameters to your `ENTRYPOINT` setting `$DOKKU_DOCKERFILE_START_CMD`. Let's say for example you are deploying a base Node.js image, with the following `ENTRYPOINT`:
By default no arguments are passed to `docker run` when deploying the container and the `CMD` or `ENTRYPOINT` defined in the `Dockerfile` are executed. You can take advantage of docker's ability to override the `CMD` or pass parameters to your `ENTRYPOINT` by setting the `dockerfile-start-cmd` property on the `ps` plugin. Let's say for example you are deploying a base Node.js image, with the following `ENTRYPOINT`:
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`)
git:allow-host <host> # Adds a host to known_hosts
git:auth <host> [<username> <password>] # Configures netrc authentication for a given git server
git:auth-status <host> [<username> <password>] # Reports whether the netrc entry matches the requested state
git:from-archive [--archive-type ARCHIVE_TYPE] <app> <archive-url> [<git-username> <git-email>] # Updates an app's git repository with a given archive file
git:from-image [--build-dir DIRECTORY] <app> <docker-image> [<git-username> <git-email>] # Updates an app's git repository with a given docker image
git:generate-deploy-key # Generates a deploy ssh key
git:load-image [--build-dir DIRECTORY] <app> <docker-image> [<git-username> <git-email>] # Updates an app's git repository with a docker image loaded from stdin
git:sync [--build|build-if-changes] <app> <repository> [<git-ref>] # Clone or fetch an app from remote git repo
git:sync [--build|--build-if-changes] [--skip-deploy-branch] <app> <repository> [<git-ref>] # Clone or fetch an app from remote git repo
git:initialize <app> # Initialize a git repository for an app
git:public-key # Outputs the dokku public deploy key
git:report [<app>] [<flag>] # Displays a git report for one or more apps
git:report [<app>] [<flag>|--format json] # Displays a git report for one or more apps
git:set <app> <key> (<value>) # Set or clear a git property for an app
git:status <app> # Show the working tree status for an app
```
@@ -163,6 +164,12 @@ When running `git:sync` without a reference, it may be useful to only build when
By default, when running `git:sync` with a git branch reference, Dokku will automatically set the `deploy-branch` property to the specified branch. To skip setting the deploy branch, specify the `--skip-deploy-branch` flag.
```shell
dokku git:sync --skip-deploy-branch node-js-app https://github.com/heroku/node-js-getting-started.git main
```
### Initializing from private repositories
> [!IMPORTANT]
@@ -185,6 +192,34 @@ dokku git:auth github.com
For syncing to a private repository stored on a remote Git product such as GitHub or GitLab, Dokku's recommendation is to use a personal access token on a bot user where possible. Please see your service's documentation for information regarding the recommended best practices.
The password for `git:auth` may also be provided over `STDIN` to avoid placing it on the command line:
The `git:auth-status` command reports whether the configured `netrc` entry matches a desired state without exposing the underlying file. It exits `0` when the configured state matches and `1` otherwise. This allows external tooling such as configuration management systems to perform idempotent updates without reading `$DOKKU_ROOT/.netrc` directly.
```shell
# check whether github.com is configured with the expected credentials
By default, the Dokku host may not have access to a server containing the remote repository. This can be initialized via the `git:allow-host` command.
@@ -235,3 +270,48 @@ dokku git:public-key
```
If there is no key, an error message is shown that displays the command that can be run on the Dokku server to generate a new public/private ssh key pair.
### Displaying git reports for an app
You can get a report about the app's git configuration using the `git:report` command:
```shell
dokku git:report
```
```
=====> node-js-app git information
Git computed deploy branch: master
Git computed keep git dir: false
Git deploy branch:
Git global archive max files: 10000
Git global archive max size: 1073741824
Git global deploy branch: master
Git keep git dir:
Git rev env var: GIT_REV
Git sha: a1b2c3d
Git source image:
Git last updated at: 1700000000
```
The `deploy-branch` and `keep-git-dir` keys hold the raw per-app value and are empty when nothing has been set on the app. The `computed-deploy-branch` and `computed-keep-git-dir` keys hold the effective value used at deploy time, falling back to the corresponding global value (where one exists) and then to the built-in default.
You can run the command for a specific app also.
```shell
dokku git:report node-js-app
```
You can pass flags which will output only the value of the specific information you want. For example:
```shell
dokku git:report node-js-app --git-deploy-branch
```
The `git:report` command also takes a `--format` flag, with the valid options including `stdout` (default) and `json`. The `json` output format can be used for automation purposes:
```shell
dokku git:report node-js-app --format json
```
The `--format` flag cannot be combined with an info flag.
@@ -17,7 +17,13 @@ In the above example, Dokku will build the app as if the repository contained _o
FROMmy-registry/node-js-getting-started:latest
```
If the specified image already exists on the Dokku host, it will not be pulled again, though this behavior may be changed using [build phase docker-options](/docs/advanced-usage/docker-options.md).
If the specified image already exists on the Dokku host, it will not be pulled again. To force a pull even when the image exists locally, use the `--force` flag:
This is useful when the remote image has been updated but the tag remains the same.
Triggering a build with the same arguments multiple times will result in Dokku exiting `0` early as there will be no changes detected. If the image tag is reused but the underlying image is different, it is recommended to use the image digest instead of the tag. This can be retrieved via the following command:
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.
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.
To import a certificate:
```shell
dokku certs:add node-js-app server.crt server.key
```
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
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.
To remove an imported certificate:
```shell
dokku certs:remove node-js-app
```
When a certificate is removed:
- The TLS secret is deleted from Kubernetes
- The app is automatically redeployed to update the ingress configuration
- If Let's Encrypt is configured, automatic certificate generation will resume
When an app is destroyed, any associated TLS secret is automatically cleaned up.
### Customizing Annotations and Labels
> [!NOTE]
@@ -412,7 +444,7 @@ To enable autoscaling, use the `app.json` `formation.$PROCESS_TYPE.autoscaling`
-`cooldown_seconds`: (default: 300) The number of seconds to wait in between each scaling event
-`triggers`: A list of autoscaling triggers.
Autoscaling triggers are passed as is to Keda, and should match the configuration keda uses for a given [scaler](https://keda.sh/docs/2.13/scalers/). Below is an example for [datadog](https://keda.sh/docs/2.13/scalers/datadog/#example-2---driving-scale-directly):
Autoscaling triggers are passed as is to Keda, and should match the configuration keda uses for a given [scaler](https://keda.sh/docs/2.19/scalers/). Below is an example for [datadog](https://keda.sh/docs/2.19/scalers/datadog/#example-2---driving-scale-directly):
```json
{
@@ -444,6 +476,8 @@ Each value in the `metadata` stanza can use the following interpolated strings:
-`DOKKU_PROCESS_TYPE`: The name of the process being scaled
-`DOKKU_APP_NAME`: The name of the app being scaled
Dokku configures a Keda `fallback` (with a failure threshold of `3` and the configured replica count) on the generated `ScaledObject` so that scaling falls back to the configured replica count when a scaler's metric source becomes unavailable. Keda only allows `fallback` when at least one trigger is something other than `cpu` or `memory`, so Dokku omits the `fallback` block when every configured trigger is a `cpu` or `memory` scaler.
##### HTTP Autoscaling
In addition to the built-in scalers that Keda provides, Dokku also supports Keda's HTTP Add On. This requires that the addon be properly installed and configured. For existing k3s clusters, this can be performed by the `scheduler-k3s:ensure-charts` command:
@@ -651,6 +685,35 @@ Alternatively, a comma separated list of chart names can be specified to only fo
K3s apps mount persistent volumes through the Dokku storage plugin. A `storage:create --scheduler k3s` call provisions a PersistentVolumeClaim through a small per-entry helm release; `storage:mount <app> <name> --container-dir <path>` then attaches it to an app. Because storage entries are global, the same PVC can be mounted into multiple apps (for `ReadWriteMany` workloads) or into the same app at multiple subpaths.
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.
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.
### Chart upgrade callbacks
Some chart version transitions require side-effects that a plain `helm upgrade` cannot perform. For example, the upgrade of `keda-add-ons-http` to `0.12.2` introduces breaking changes to deployment selectors, which are immutable in Kubernetes; the chart-managed deployments must be deleted before the upgrade can proceed.
Dokku handles these cases internally by registering version-targeted pre-upgrade and post-upgrade callbacks against affected charts. When `scheduler-k3s:ensure-charts` runs, the installed chart version is compared against the registered callbacks: any whose target version is greater than the currently installed version and at most the configured chart version are executed in ascending semver order, each bracketed around an upgrade to that intermediate version. A final upgrade to the configured chart version then completes the run.
These callbacks are not user-configurable. Failures during a callback or its surrounding upgrade abort the run, and a subsequent `scheduler-k3s:ensure-charts` resumes from the current state.
## Scheduler Interface
The following sections describe implemented and unimplemented scheduler functionality for the `k3s` scheduler.
@@ -710,6 +773,7 @@ Next, run the `scheduler-k3s:ensure-charts` command with the `vector` chart to f
```shell
dokku scheduler-k3s:ensure-charts --charts vector
```
Please see the [vector logs documentation](/docs/deployment/logs.md#configuring-a-log-sink) for more information on specifying vector sinks.
@@ -45,6 +45,26 @@ The default value may be set by passing an empty value for the option.
dokku scheduler:set --global selected
```
### Setting the app shell
The shell used for `dokku run` and `dokku enter` invocations against an app can be customized via the `shell` property. This is useful for images that ship with a non-default shell - for example, alpine-based images that only have `/bin/sh`.
```shell
dokku scheduler:set node-js-app shell sh
```
The `shell` property can also be set globally:
```shell
dokku scheduler:set --global shell sh
```
The default value (an empty string, which lets the scheduler pick its own default) may be restored by passing an empty value:
```shell
dokku scheduler:set node-js-app shell
```
### Displaying scheduler reports for an app
You can get a report about the app's scheduler status using the `scheduler:report` command:
@@ -28,7 +28,9 @@ You may both create user-defined checks for web processes using the `healthcheck
### wait-to-retire
After a successful deploy, the grace period given to old containers before they are stopped/terminated is determined by the value of `wait-to-retire`. This is useful for ensuring completion of long-running HTTP connections.
After a successful deploy, Dokku immediately sends `SIGTERM` to old containers so the application can begin a graceful shutdown (draining in-flight connections, flushing buffers, and so on) the moment proxy traffic switches to the new containers. The old containers are then left running for the `wait-to-retire` grace period before being stopped, which sends `SIGTERM` again and ultimately `SIGKILL` after `stop-timeout-seconds`. This mirrors Heroku's [dyno shutdown behavior](https://devcenter.heroku.com/articles/dyno-shutdown-behavior).
The `wait-to-retire` value controls the grace period and is useful for ensuring completion of long-running HTTP connections.
```shell
dokku checks:set node-js-app wait-to-retire 30
@@ -40,17 +42,17 @@ Defaults to `60`.
You can set the `stop-timeout-seconds` property on the `ps` plugin to change this value (default: `30`). See the [process management documentation](/docs/processes/process-management.md#changing-process-management-settings) for more information.
## Configuring check settings using the `config` plugin
## Configuring check settings
There are certain settings that can be configured via environment variables:
There are certain settings that can be configured via the `checks` plugin properties:
-`DOKKU_DEFAULT_CHECKS_WAIT`: (default: `10`) If no user-defined checks are specified - or if the process being checked is not a `web` process - this is the period of time Dokku will wait before checking that a container is still running.
-`default-wait`: (default: `10`) If no user-defined checks are specified - or if the process being checked is not a `web` process - this is the period of time Dokku will wait before checking that a container is still running. Set globally via `dokku checks:set --global default-wait <value>`.
The following settings may also be specified in the `app.json` file, though are available as environment variables in order to ease application reuse.
The following settings may also be specified in the `app.json` file, though are available as properties in order to ease application reuse.
-`DOKKU_CHECKS_WAIT`: (default: `5`) Wait this many seconds for the container to start before running checks.
-`DOKKU_CHECKS_TIMEOUT`: (default: `30`) Wait this many seconds for each response before marking it as a failure.
-`DOKKU_CHECKS_ATTEMPTS`: (default: `5`) Number of retries for to run for a specific check before marking it as a failure
-`wait`: (default: `5`) Wait this many seconds for the container to start before running checks. Set via `dokku checks:set <app> wait <value>`.
-`timeout`: (default: `30`) Wait this many seconds for each response before marking it as a failure. Set via `dokku checks:set <app> timeout <value>`.
-`attempts`: (default: `5`) Number of retries for to run for a specific check before marking it as a failure. Set via `dokku checks:set <app> attempts <value>`.
-----> Skipping zero downtime for app's (node-js-app) proctypes (worker,web)
-----> Unsetting node-js-app
-----> Unsetting DOKKU_CHECKS_DISABLED
-----> Setting config vars
DOKKU_CHECKS_SKIPPED: worker,web
```
Zero downtime checks can also be disabled completely. This will stop old containers *before* new ones start, which may result in broken connections and downtime if your application fails to boot properly.
This document provides a developer-focused overview of Dokku's internal architecture. It is intended for contributors who want to understand how Dokku works under the hood before diving into the codebase.
## High-Level Architecture
Dokku is a Docker-powered Platform as a Service (PaaS) that provides a Heroku-like deployment experience. The following diagram shows the main components and their interactions:
```
+------------------+
| User/Client |
+--------+---------+
|
| SSH / git push
v
+--------+---------+
| dokku binary |
| (bash script) |
+--------+---------+
|
| parse_args / execute_dokku_cmd
v
+--------+---------+
| Plugin System |
| (plugn) |
+--------+---------+
|
| triggers / subcommands
v
+--------+---------+ +------------------+
| Core Plugins |---->| Docker / K8s |
| (apps, git, | | Runtime |
| config, etc.) | +------------------+
+------------------+
```
**Key components:**
- **dokku binary**: The main entry point (`/dokku`), a bash script that handles authentication, argument parsing, and routes commands to plugins
- **Plugin System**: Uses [plugn](https://github.com/dokku/plugn) to execute triggers and discover commands
Dokku's functionality is entirely implemented through plugins. This architecture provides:
- **Extensibility**: Add new features without modifying core code
- **Loose coupling**: Plugins communicate via well-defined triggers
- **Composability**: Mix and match builders, schedulers, and proxies
### Plugin Communication via Triggers
Plugins communicate through the **trigger system** powered by [plugn](https://github.com/dokku/plugn). When a trigger is fired, plugn executes matching scripts from all enabled plugins:
```
+---------------+ plugn trigger +---------------+
| Plugin A | -------------------> | Plugin B |
| (fires event) | "post-deploy" | (listens for |
+---------------+ | event) |
+---------------+
|
v
+---------------+
| Plugin C |
| (also listens)|
+---------------+
```
**Trigger execution flow:**
1. A plugin calls `plugn trigger <trigger-name> [args...]`
2. plugn searches `$PLUGIN_ENABLED_PATH/*/` for files named `<trigger-name>`
3. Each matching executable is run with the provided arguments
4. Triggers can return data via stdout or signal errors via exit codes
| **Plugin-based architecture** | Enables extensibility without modifying core code. Community plugins can add databases, caching, and other services. |
| **Bash + Go hybrid** | Bash for orchestration and simple commands; Go for performance-critical operations and complex logic. |
| **Trigger system** | Loose coupling between plugins. Plugins don't need to know about each other; they just fire and respond to events. |
| **File-based state** | Simple, transparent, and easy to debug. No database dependency. State can be inspected with standard Unix tools. |
| **Docker as foundation** | Leverages Docker's container runtime, networking, and image management. Allows multiple scheduler backends. |
## Further Reading
- [Plugin Creation](/docs/development/plugin-creation.md) - How to create custom plugins
- [Plugin Triggers](/docs/development/plugin-triggers.md) - Complete list of available triggers
- [Testing](/docs/development/testing.md) - How to test Dokku and plugins
> To avoid issues with community plugins, this plugin trigger should be used _only_ for core plugins. Please avoid using this trigger in your own plugins.
- Description: `$PROC_TYPE` may be set to magic `_all_` process type to signify global docker deploy options.
- Description: emits docker arguments scoped to a specific Procfile process type. The `docker-options` plugin implements this trigger to surface options registered via `docker-options:add --process <PROC>`.`$PROC_TYPE` may be empty (or set to the magic `_default_` value) to signify default-scope docker deploy options.
- Invoked by: `dokku storage:list` and `dokku deploy`
> [!WARNING]
> Deprecated as of 0.38.0. The trigger handler still functions for back-compat
> with external plugins but emits a deprecation warning on every invocation.
> In-process callers should use the `storage-app-mounts` trigger instead, or
> the `storage` Go package directly.
- Description: Returns a list of storage mounts for an app
- Invoked by: external plugins (the core `storage:list` subcommand no longer uses it)
- Arguments: `$APP $PHASE $FORMAT`
- Example:
```shell
#!/usr/bin/env bash
### `storage-app-mounts`
set -eo pipefail;[[$DOKKU_TRACE]]&&set -x
APP="$1"
- Description: Returns the (entry, attachment) pairs an app has for the given phase, in JSON. Schedulers consume this to render volume mounts.
- Invoked by: scheduler plugins during deploy
- Arguments: `$APP $PHASE`
- Output: a JSON array of objects shaped `{"entry": Entry, "attachment": Attachment}`
# TODO
```
### `storage-create`
- Description: Provisions or updates the underlying volume for a k3s storage entry. Called when `storage:create` or `storage:set` runs against a k3s entry. Receives the JSON-encoded entry on stdin.
- Description: Releases the underlying volume for a k3s storage entry. Called when `storage:destroy` runs against a k3s entry. Receives the JSON-encoded entry on stdin.
- Invoked by: `dokku storage:destroy`
- Arguments: `$ENTRY_NAME`
- Stdin: JSON-encoded `Entry` payload
### `storage-status`
- Description: Returns the current status of a storage entry's underlying volume. Used by `storage:wait` to poll the cluster. The handler should print one line: `Bound`, `Pending`, or `Lost` for k3s entries.
- Invoked by: `dokku storage:wait`
- Arguments: `$ENTRY_NAME`
- Stdin: JSON-encoded `Entry` payload
### `scheduler-storage-exec`
- Description: Runs an interactive or non-interactive command against a storage entry. The storage plugin fires this with `<scheduler>` as the first arg; each scheduler plugin's handler matches against its own scheduler name and either handles or no-ops, mirroring `scheduler-deploy` / `scheduler-app-status`. Plugn forwards stdin/stdout/stderr to the handler subprocess so an interactive shell streams cleanly. The handler exits with the underlying tool's status code so `dokku storage:exec` propagates exit codes verbatim.
@@ -27,7 +27,7 @@ Alternatively, you can use `docker-compose.yml`:
```yaml
services:
dokku:
image:dokku/dokku:0.37.2
image:dokku/dokku:0.38.4
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.37.2
FROMdokku/dokku:0.38.4
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
@@ -39,7 +39,7 @@ The Caddy plugin has specific rules for routing requests:
To use the Caddy plugin, use the `proxy:set` command for the app in question:
```shell
dokku proxy:set node-js-app caddy
dokku proxy:set node-js-apptype caddy
```
This will enable the docker label-based Caddy integration. All future deploys will inject the correct labels for Caddy to read and route requests to containers. Due to the docker label-based integration used by Caddy, a single deploy or rebuild will be required before requests will route successfully.
@@ -35,7 +35,7 @@ The Haproxy plugin has specific rules for routing requests:
To use the Haproxy plugin, use the `proxy:set` command for the app in question:
```shell
dokku proxy:set node-js-app haproxy
dokku proxy:set node-js-apptype haproxy
```
This will enable the docker label-based Haproxy integration. All future deploys will inject the correct labels for Haproxy to read and route requests to containers. Due to the docker label-based integration used by Haproxy, a single deploy or rebuild will be required before requests will route successfully.
After modifying, the Haproxy container will need to be restarted.
### Changing the Haproxy refresh interval
Haproxy polls the Docker API for label changes every `10` seconds by default. The interval may be changed by setting the `refresh-conf` property with the `--global` flag:
```shell
dokku haproxy:set --global refresh-conf 5
```
The `refresh-conf` property is global-only and cannot be set on a per-app basis. Setting an empty value will reset it to the default. After modifying, the Haproxy container will need to be restarted.
### Label Management
The Haproxy plugin allows you to add custom container labels to apps. These labels are injected into containers during deployment and can be used to configure Haproxy behavior beyond what the plugin provides by default.
@@ -27,6 +28,21 @@ Nginx will proxy the requests in a [round-robin balancing fashion](http://nginx.
> [!NOTE]
> Due to how the plugin is implemented, if an app successfully starts up `web` containers but fails to deploy some other containers, nginx may eventually stop routing requests. Users should revert their code in these cases, or manually trigger `dokku proxy:build-config $APP` in order to ensure requests route to the new web containers.
### Nginx Configuration for Undeployed Apps
> [!IMPORTANT]
> New as of 0.38.0
When an app is created but not yet deployed, has no `web` process type, or has no running web processes, Dokku generates a minimal nginx configuration that returns `502 Bad Gateway` responses. This ensures that:
- The app's domain resolves and returns a non-200 status code, allowing monitoring tools to detect the issue.
- SSL certificate provisioning tools such as letsencrypt can function, as an nginx server is listening on the domain.
- The `nginx.conf.d/` include directory is available for plugin customization.
The 502 error page includes auto-retry JavaScript that will automatically reload the page when the application becomes available.
Once the app is deployed with running `web` processes, the placeholder configuration is automatically replaced with the full proxy configuration.
### Starting nginx
> [!IMPORTANT]
@@ -49,6 +65,19 @@ The nginx server can be stopped via `nginx:stop`.
dokku nginx:stop
```
### Reloading nginx
> [!IMPORTANT]
> New as of 0.38.0
The nginx server can be reloaded via `nginx:reload`. This validates the current nginx configuration and, if valid, signals nginx to reload without restarting the running process. This is the preferred command after editing files in `/etc/nginx/conf.d/` directly.
```shell
dokku nginx:reload
```
If the configuration is invalid, the command exits non-zero and prints the validation error without reloading.
It may 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.
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).
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
dokku nginx:validate-config
@@ -113,6 +144,34 @@ The `--clean` flag may also be specified for a given app:
dokku nginx:validate-config node-js-app --clean
```
### Displaying nginx reports for an app
You can get a report about the app's nginx configuration using the `nginx:report` command:
```shell
dokku nginx:report
```
You can run the command for a specific app also.
```shell
dokku nginx:report node-js-app
```
You can pass flags which will output only the value of the specific information you want. For example:
The `nginx:report` command also takes a `--format` flag, with the valid options including `stdout` (default) and `json`. The `json` output format can be used for automation purposes:
```shell
dokku nginx:report node-js-app --format json
```
The `--format` flag cannot be combined with an info flag.
### Custom Error Pages
By default, Dokku provides custom error pages for the following three categories of errors:
@@ -125,52 +184,107 @@ These are provided as an alternative to the generic Nginx error page, are shared
### Default site
By default, Dokku will route any received request with an unknown HOST header value to the lexicographically first site in the nginx config stack. This means that accessing the dokku server via its IP address or a bogus domain name may return a seemingly random website.
> [!IMPORTANT]
> New as of 0.38.0
> [!WARNING]
> Some versions of Nginx may create a default site when installed. This site is simply a static page which says "Welcome to Nginx", and if this default site is enabled, Nginx will not route any requests with an unknown HOST header to Dokku. If you want Dokku to receive all requests, run the following commands:
>
> ```
> rm /etc/nginx/sites-enabled/default
> dokku nginx:stop
> dokku nginx:start
> ```
On fresh apt installs, Dokku ships a catch-all default site at `/etc/nginx/conf.d/00-default-vhost.conf` that rejects requests whose Host header (or TLS SNI) does not match any deployed app. The catch-all uses [`ssl_reject_handshake on`](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_reject_handshake) for HTTPS and [`return 444`](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return) for HTTP. Both close the connection without sending a response.
If services should only be accessed via their domain name, you may want to disable the default site by adding the following configuration to the global nginx configuration.
Create the file at `/etc/nginx/conf.d/00-default-vhost.conf`:
The shipped file looks like:
```nginx
server{
listen80default_server;
listen[::]:80default_server;
# If services hosted by dokku are available via HTTPS, it is recommended
# to also uncomment the following section.
#
# Please note that in order to let this work, you need an SSL certificate. However
# it does not need to be valid. Users of Debian-based distributions can install the
# `ssl-cert` package with `sudo apt install ssl-cert` to automatically generate
# a self-signed certificate that is stored at `/etc/ssl/certs/ssl-cert-snakeoil.pem`.
Make sure to reload nginx after creating this file by running `systemctl reload nginx.service`.
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.
This will catch all unknown HOST header values and close the connection without responding. You can replace the `return 444;` with `return 410;` which will cause nginx to respond with an error page.
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:
The configuration file must be loaded before `/etc/nginx/conf.d/dokku.conf`, so it can not be arranged as a vhost in `/etc/nginx/sites-enabled` that is only processed afterwards.
```nginx
server{
listen80default_server;
listen[::]:80default_server;
Alternatively, you may push an app to your Dokku host with a name like "00-default". As long as it lists first in `ls /home/dokku/*/nginx.conf | head`, it will be used as the default nginx vhost.
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.
| 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 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`. |
#### Conflicting upstream nginx default vhost
A fresh nginx install ships its own default vhost that also claims port 80 with `default_server`, which would cause `nginx -t` to fail with `a duplicate default server for 0.0.0.0:80` once the dokku catch-all is in place. To avoid this, the dokku postinst renames any of the following files in place to `${path}.dokku-disabled` instead of deleting them, preserving any local customizations:
-`/etc/nginx/sites-enabled/default`
-`/etc/nginx/sites-available/default`
-`/etc/nginx/conf.d/default.conf`
To recover an original file, inspect the `.dokku-disabled` sibling.
#### Disabling the catch-all on first install
Select "No" at the `dokku/install_default_site` debconf prompt, or pre-seed the answer before installing:
#### Installing the catch-all on upgraded or non-apt installs
Existing dokku installs upgraded from earlier versions do not get the catch-all installed automatically; the upgrade leaves their nginx configuration untouched. To install it manually, copy the shipped template into place and reload nginx:
If the upstream nginx default vhost is also active, disable it first (e.g. `mv /etc/nginx/sites-enabled/default{,.disabled}`) to avoid a duplicate-default-server error.
#### App-name based alternative
As an alternative to the catch-all file, you may push an app to your Dokku host with a name like `00-default`. As long as it lists first in `ls /home/dokku/*/nginx.conf | head`, it will be used as the default nginx vhost.
### Customizing the nginx configuration
@@ -235,8 +349,8 @@ Unsetting this value is the same as enabling custom nginx config usage.
{{ .APP }} Application name
{{ .APP_SSL_PATH }} Path to SSL certificate and key
{{ .DOKKU_ROOT }} Global Dokku root directory (ex: app dir would be `{{ .DOKKU_ROOT }}/{{ .APP }}`)
{{ .PROXY_PORT }} Non-SSL nginx listener port (same as `DOKKU_PROXY_PORT` config var)
{{ .PROXY_SSL_PORT }} SSL nginx listener port (same as `DOKKU_PROXY_SSL_PORT` config var)
{{ .PROXY_PORT }} Non-SSL nginx listener port (same as the `proxy-port` property)
{{ .PROXY_SSL_PORT }} SSL nginx listener port (same as the `proxy-ssl-port` property)
{{ .NOSSL_SERVER_NAME }} List of non-SSL VHOSTS
{{ .PROXY_PORT_MAP }} List of port mappings (same as the `map` ports property)
{{ .PROXY_UPSTREAM_PORTS }} List of configured upstream ports (derived from the `map` ports property)
@@ -338,6 +452,7 @@ Changing these value globally or on a per-app basis will require rebuilding the
| proxy-buffers | `8 8k` | string | Number and size of the buffers used for reading the proxied server response, for a single connection |
| proxy-busy-buffers-size | `16k` | string | Limits the total size of buffers that can be busy sending a response to the client while the response is not yet fully read. |
| proxy-connect-timeout | `60s` | string | Timeout (with units) for establishing a connection to your backend server |
| proxy-keepalive | empty (disabled) | integer | Number of idle keepalive connections to upstream servers (disabled by default) |
| proxy-read-timeout | `60s` | string | Timeout (with units) for reading response from your backend server |
| proxy-send-timeout | `60s` | string | Timeout (with units) for transmitting a request to your backend server |
| send-timeout | `60s` | string | Timeout (with units) for transmitting a response to your the client |
@@ -35,7 +35,7 @@ The OpenResty plugin has specific rules for routing requests:
To use the OpenResty plugin, use the `proxy:set` command for the app in question:
```shell
dokku proxy:set node-js-app openresty
dokku proxy:set node-js-apptype openresty
```
This will enable the docker label-based OpenResty integration. All future deploys will inject the correct labels for OpenResty to read and route requests to containers. Due to the docker label-based integration used by OpenResty, a single deploy or rebuild will be required before requests will route successfully.
@@ -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.
@@ -32,13 +32,49 @@ The Traefik plugin has specific rules for routing requests:
- If no `https:443` mapping is found, the first `https` port mapping is used for https requests.
- If no `https` mapping is found, the container port from `http:80` will be used for https requests.
- Requests are routed as soon as the container is running and passing healthchecks.
- Readiness healthchecks defined in `app.json` with a `path` property are automatically transformed into Traefik healthcheck labels.
### Healthchecks
When an app has a readiness healthcheck defined in its `app.json` file with a `path` property, Dokku automatically generates Traefik healthcheck labels. These labels configure Traefik to perform health checks on the container before routing traffic to it.
The following `app.json` healthcheck properties are mapped to Traefik labels:
| `path` | `healthcheck.path` | The HTTP path to check (required) |
| `scheme` | `healthcheck.scheme` | The scheme to use (`http` or `https`) |
| `port` | `healthcheck.port` | The port to check |
| `timeout` | `healthcheck.timeout` | Timeout in seconds (formatted as `Xs`) |
| `wait` | `healthcheck.interval` | Interval between checks in seconds (formatted as `Xs`) |
Example `app.json` configuration:
```json
{
"healthchecks":{
"web":[
{
"name":"web readiness check",
"path":"/health",
"timeout":5,
"type":"readiness",
"wait":10
}
]
}
}
```
> [!NOTE]
> Only the first readiness healthcheck with a `path` property is used. Liveness, startup, and command-based healthchecks are not transformed into Traefik labels.
### Switching to Traefik
To use the Traefik plugin, use the `proxy:set` command for the app in question:
```shell
dokku proxy:set node-js-app traefik
dokku proxy:set node-js-apptype traefik
```
This will enable the docker label-based Traefik integration. All future deploys will inject the correct labels for Traefik to read and route requests to containers. Due to the docker label-based integration used by Traefik, a single deploy or rebuild will be required before requests will route successfully.
After enabling, the Traefik container will need to be restarted and apps will need to be rebuilt to retrieve certificates from the new server.
#### Switching to DNS-01 challenge mode
By default, Traefik uses TLS-ALPN-01 challenge for obtaining certificates. To switch to DNS-01 challenge mode (useful for wildcard certificates or when port 443 is not accessible), you need to:
3. Configure the required environment variables for your DNS provider. Each DNS provider requires specific environment variables. The variable names should be prefixed with `dns-provider-`:
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`.
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.
To switch back to TLS challenge mode:
```shell
dokku traefik:set --global challenge-mode tls
```
### API Access
Traefik exposes an API and Dashboard, which Dokku disables by default for security reasons. It can be exposed and customized as described below.
@@ -9,7 +9,7 @@ proxy:clear-config [--all|<app>] # Clears config for given app
proxy:disable [--parallel count] [--all|<app>] # Disable proxy for app
proxy:enable [--parallel count] [--all|<app>] # Enable proxy for app
proxy:report [<app>] [<flag>] # Displays a proxy report for one or more apps
proxy:set [<app>|--global] <proxy-type> # Set proxy type for app
proxy:set [<app>|--global] <key> (<value>) # Set or clear a proxy property for an app
```
In Dokku 0.5.0, port proxying was decoupled from the `nginx-vhosts` plugin into the proxy plugin. Dokku 0.6.0 introduced the ability to map host ports to specific container ports. This allows other proxy software - such as HAProxy or Caddy - to be used in place of nginx.
@@ -21,27 +21,69 @@ In Dokku 0.5.0, port proxying was decoupled from the `nginx-vhosts` plugin into
The default proxy shipped with Dokku is `nginx`. It can be changed via the `proxy:set` command.
```shell
dokku proxy:set node-js-app caddy
dokku proxy:set node-js-apptype caddy
```
```
-----> Setting config vars
DOKKU_APP_PROXY_TYPE: caddy
=====> Setting type to caddy
```
The proxy may also be set on a global basis. This is usually preferred as running multiple proxy implementations may cause port collision issues.
```shell
dokku proxy:set --global caddy
dokku proxy:set --globaltype caddy
```
```
-----> Setting config vars
DOKKU_PROXY_TYPE: caddy
=====> Setting type to caddy
```
Changing the proxy does not stop or start any given proxy implementation. Please see the documentation for your proxy implementation for details on how to perform a change.
### Disabling and enabling the proxy
The proxy can be disabled on a per-app basis via the `proxy:disable` command. While disabled, no proxy configuration will be generated for the app and incoming requests will not be routed through the configured proxy.
```shell
dokku proxy:disable node-js-app
```
```
-----> Disabling proxy for app
```
The proxy can be re-enabled with the `proxy:enable` command:
```shell
dokku proxy:enable node-js-app
```
```
-----> Enabling proxy for app
```
### Setting proxy ports
When an app's domains are disabled (see [domains documentation](/docs/configuration/domains.md)), Dokku still exposes the app on a high port so that internal services can reach it at a stable port across deploys. The non-SSL and SSL ports used for this can be configured via the `proxy:set` command:
```shell
dokku proxy:set node-js-app proxy-port 5000
dokku proxy:set node-js-app proxy-ssl-port 5443
```
Either property may also be set globally. Per-app values take precedence over the global value.
```shell
dokku proxy:set --global proxy-port 5000
dokku proxy:set --global proxy-ssl-port 5443
```
The default value for either property may be restored by passing an empty value:
```shell
dokku proxy:set node-js-app proxy-port
```
### Regenerating proxy config
In certain cases, your app proxy configs may drift from the correct config for your app. You may regenerate the config at any point via the `proxy:build-config` command. This command will trigger a rebuild for the configured proxy implementation (default: nginx) for a given app. The command may fail if there are no current web listeners for your app.
For Dockerfile-built apps, set `dockerfile-start-cmd`. This value is passed as arguments to `docker run` and overrides or supplements the image's `CMD` / `ENTRYPOINT`. See the [Dockerfile builder documentation](/docs/deployment/builders/dockerfiles.md#customizing-the-run-command) for context.
Either property can be cleared by passing an empty value:
```shell
dokku ps:set node-js-app start-cmd
```
### Restart policies
> [!IMPORTANT]
@@ -429,3 +451,15 @@ When a server reboots or Docker is restarted/upgraded, Docker may or may not sta
- If any of the app containers are missing, the entire app will be rebuilt.
During this time, requests may route to the incorrect app if the assigned IPs correspond to those for other apps. While dokku makes all efforts to avoid this, there may be a few minutes where urls may route to the wrong app. To avoid this, either use a custom proxy plugin or wait a few minutes until the restoration process is complete.
Per-app restore behavior can be controlled via the `restore` property. When set to `false`, `ps:restore` will skip the app on reboot:
```shell
dokku ps:set node-js-app restore false
```
The default value (`true`) may be restored by passing an empty value:
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.