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.
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.
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.
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 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.
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.
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.
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.