# History
## 0.38.21
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.21/bootstrap.sh
sudo DOKKU_TAG=v0.38.21 bash bootstrap.sh
```
### New Features
- #8795: @josegonzalez Add --format json support to apps:list
### Tests
- #8788: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.81.1 to 1.82.0 in /tests/apps/gogrpc
- #8787: @dependabot[bot] chore(deps): bump golang.org/x/net from 0.51.0 to 0.55.0 in /tests/apps/gogrpc
### Dependencies
- #8781: @dependabot[bot] chore(deps): bump lucaslorentz/caddy-docker-proxy from 2.12 to 2.13 in /plugins/caddy-vhosts
- #8794: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.36.0 to 1.37.0 in /plugins/scheduler-k3s
- #8789: @dependabot[bot] chore(deps): bump pymdown-extensions from 11.0 to 11.0.1 in /docs/_build
- #8782: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.23.2 to 0.24.0 in /plugins/scheduler-k3s
- #8785: @dependabot[bot] chore(deps): bump traefik from v3.7.5 to v3.7.6 in /plugins/traefik-vhosts
- #8786: @dependabot[bot] chore(deps): bump github.com/fluxcd/pkg/kustomize from 1.31.0 to 1.36.0 in /plugins/scheduler-k3s
- #8783: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.50 to 2.11.51 in /plugins/scheduler-k3s
- #8780: @dependabot[bot] chore(deps): bump github.com/cert-manager/cert-manager from 1.20.2 to 1.20.3 in /plugins/scheduler-k3s
- #8779: @dependabot[bot] chore(deps): bump github.com/go-openapi/jsonpointer from 0.23.1 to 0.23.2 in /plugins/scheduler-k3s
### Other
- #8796: @josegonzalez Add --format json support to ps:scale
# History
## 0.38.20
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.20/bootstrap.sh
sudo DOKKU_TAG=v0.38.20 bash bootstrap.sh
```
### Tests
- #8773: @dependabot[bot] chore(deps): bump python from 3.15.0b2-bookworm to 3.15.0b3-bookworm in /tests/apps/dockerfile-release
- #8756: @dependabot[bot] chore(deps): bump sass from 1.100.0 to 1.101.0 in /tests/apps/multi
- #8753: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 291 to 292 in /tests/apps/php
### Dependencies
- #8766: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.21.1 to 3.21.2 in /plugins/scheduler-k3s
- #8777: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.1 in /plugins/common
- #8778: @dependabot[bot] chore(deps): bump byjg/easy-haproxy from 6.1.0 to 6.1.1 in /plugins/haproxy-vhosts
- #8748: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.52.0 to 0.53.0 in /plugins/common
- #8745: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0 in /plugins/common
- #8774: @dependabot[bot] chore(deps): bump python from 3.15.0b2-alpine to 3.15.0b3-alpine in /docs/_build
- #8769: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.21.3 to 11.0 in /docs/_build
- #8775: @dependabot[bot] chore(deps): bump click from 8.4.1 to 8.4.2 in /docs/_build
- #8776: @dependabot[bot] chore(deps): bump byjg/easy-haproxy from 6.0.1 to 6.1.0 in /plugins/haproxy-vhosts
- #8772: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.42.0 to 1.42.1 in /plugins/buildpacks
- #8770: @dokku-bot chore: bump pack to 0.40.7
- #8767: @dependabot[bot] chore(deps): bump github.com/containerd/containerd from 1.7.32 to 1.7.33 in /plugins/scheduler-k3s
- #8765: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.0 in /plugins/scheduler-k3s
- #8764: @dependabot[bot] chore(deps): bump actions/checkout from 6 to 7
- #8762: @dependabot[bot] chore(deps): bump k8s.io/kubernetes from 1.36.1 to 1.36.2 in /plugins/scheduler-k3s
- #8758: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.0 in /plugins/buildpacks
- #8760: @dependabot[bot] chore(deps): bump k8s.io/apimachinery from 0.36.1 to 0.36.2 in /plugins/scheduler-k3s
- #8757: @dependabot[bot] chore(deps): bump helm.sh/helm/v3 from 3.21.0 to 3.21.1 in /plugins/scheduler-k3s
- #8755: @dependabot[bot] chore(deps): bump traefik from v3.7.4 to v3.7.5 in /plugins/traefik-vhosts
- #8754: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.49 to 2.11.50 in /plugins/scheduler-k3s
- #8749: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.47 to 2.11.49 in /plugins/scheduler-k3s
- #8743: @dependabot[bot] chore(deps): bump beautifulsoup4 from 4.14.3 to 4.15.0 in /docs/_build
- #8744: @dependabot[bot] chore(deps): bump traefik from v3.7.3 to v3.7.4 in /plugins/traefik-vhosts
- #8747: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0 in /plugins/scheduler-k3s
- #8746: @dependabot[bot] chore(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0 in /plugins/scheduler-docker-local
# History
## 0.38.18
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.18/bootstrap.sh
sudo DOKKU_TAG=v0.38.18 bash bootstrap.sh
```
### Tests
- #8734: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/gogrpc
- #8731: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/go-fail-predeploy
- #8733: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/zombies-dockerfile-tini
- #8735: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/go-fail-postdeploy
- #8736: @dependabot[bot] chore(deps): bump golang from 1.26.3 to 1.26.4 in /tests/apps/zombies-dockerfile-no-tini
- #8738: @dependabot[bot] chore(deps): bump python from 3.15.0b1-bookworm to 3.15.0b2-bookworm in /tests/apps/dockerfile-release
- #8730: @dependabot[bot] chore(deps): bump django from 5.2.14 to 5.2.15 in /tests/apps/dockerfile-release
### Dependencies
- #8727: @dependabot[bot] chore(deps): bump github.com/melbahja/goph from 1.5.0 to 1.5.1 in /plugins/common
- #8739: @dependabot[bot] chore(deps): bump github.com/traefik/traefik/v2 from 2.11.46 to 2.11.47 in /plugins/scheduler-k3s
- #8737: @dependabot[bot] chore(deps): bump dokku/openresty-docker-proxy from 0.12.0 to 0.12.1 in /plugins/openresty-vhosts
- #8732: @dependabot[bot] chore(deps): bump python from 3.15.0b1-alpine to 3.15.0b2-alpine in /docs/_build
- #8740: @dependabot[bot] chore(deps): bump timberio/vector from 0.55.0-debian to 0.56.0-debian in /plugins/logs
- #8741: @dependabot[bot] chore(deps): bump traefik from v3.7.1 to v3.7.3 in /plugins/traefik-vhosts
storage:report gains a flat dotted key per attachment field (entry-name, host-path, container-path, phases, process-type, subpath, readonly, volume-options, volume-chown) under the --storage-attachment.<index>.<field> shape. Both stdout and JSON pick the keys up through the existing common.ReportSingleApp pipeline. Attachments that reference a missing storage entry emit a warning and are skipped, so the rest of the report still renders. Closes#8710.
Re-running `storage:mount <app> <entry> --container-dir <path>` against an existing `(entry, container_dir, process_type)` tuple now updates the attachment's mount-time fields in place instead of erroring with `already mounted`. Declarative tooling that wants to change `--volume-options`, `--volume-chown`, `--phase`, `--volume-subpath`, or `--volume-readonly` on an existing mount no longer has to unmount-then-remount, which briefly dropped the volume from `storage:report` and raced against any deploy that fired in the window. The legacy `host:container[:opts]` form keeps its strict `Mount path already exists.` failure.
The `storage:list <app> --format json` payload conflated the
attachment's `Readonly` flag and its `VolumeOptions` field into a
single derived `volume_options` string that rendered as `ro`,
`<options>`, or `ro,<options>` depending on which fields were set on
the underlying attachment. That shape is fine for the legacy
`host:container[:options]` text view but it leaves drift-detection
tooling unable to tell whether a `ro` token came from
`Attachment.Readonly == true` or from the operator setting
`Attachment.VolumeOptions = "ro"` directly. The JSON now exposes
`readonly` (boolean) and `volume_options` (string) as separate
omitempty keys populated straight from the attachment, and
`formatStorageListEntry` combines them at format time for the colon-
form text view. `ParseMountPath` was extended in lockstep so callers
of the legacy `host:container:opts` form no longer have to special-
case the `ro` token themselves.
The named-entry form of `storage:mount` had no way to set the attachment's `volume_options`, even though the legacy `host:container:opts` colon form parses options into the same field and every downstream consumer (the docker-args trigger and the storage:list/storage:report display path) already renders them. Tooling that declaratively re-applies attachments silently dropped options on every re-apply.
storage:create --chown invoked chown-storage-dir with the full host path, but the helper validates a basename and prepends the storage root itself, so every call failed with `Directory can only contain the following set of characters`. Pass the entry name and reject the combination of --chown with a non-default host path, since the helper only manages the default storage location.
# History
## 0.38.10
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.10/bootstrap.sh
sudo DOKKU_TAG=v0.38.10 bash bootstrap.sh
```
### New Features
- #8702: @josegonzalez Prompt for confirmation on storage:destroy
### Tests
- #8698: @dependabot[bot] chore(deps-dev): bump org.apache.maven.plugins:maven-dependency-plugin from 3.10.0 to 3.11.0 in /tests/apps/java
`storage:destroy` now prompts for confirmation before removing a named storage entry, matching the behavior of other destructive commands such as `apps:destroy` and `network:destroy`. The prompt can be skipped with the `--force` flag or the global `dokku --force` flag for non-interactive callers.
Every Go-implemented plugin's `:report --format json` now emits keys without the redundant `<plugin>-` head segment, matching the shape bash plugins have always emitted. The CLI flag names and `:set` semantics are unchanged. For backwards compatibility during the 0.38.x patch series, the legacy `<plugin>-<property>` keys are emitted side-by-side with the new keys, and a future major release will drop the legacy keys. `common.ReportSingleApp` is refactored to accept a `ReportSingleAppInput` struct with a `Validate()` method so the input is checked before any work runs, which also catches the latent `"docker options"` reportType bug at the API boundary.
# History
## 0.38.7
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.7/bootstrap.sh
sudo DOKKU_TAG=v0.38.7 bash bootstrap.sh
```
### Security
- #8672: @josegonzalez Prevent host shell injection from app.json cron commands
### Bug Fixes
- #8669: @josegonzalez Recover deployed image from registry on local miss
- #8679: @josegonzalez Align ps/cron :report --global keys with plugin convention
- #8678: @josegonzalez Split openresty report keys into global and computed pairs
- #8676: @immanuwell Use SYSTEM for shfmt Darwin detection
### New Features
- #8677: @josegonzalez Warn on deprecated listen http2 in custom nginx templates
### Tests
- #8683: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 290 to 291 in /tests/apps/php
- #8671: @josegonzalez Qualify scheduler-k3s ingressroute kubectl lookups
### Dependencies
- #8682: @dependabot[bot] chore(deps): bump k8s.io/apimachinery from 0.36.0 to 0.36.1 in /plugins/scheduler-k3s
- #8681: @dependabot[bot] chore(deps): bump soupsieve from 2.8.3 to 2.8.4 in /docs/_build
### Other
- #8680: @josegonzalez Split more report global keys into raw and computed
Adds an app-aware `fn-verify-app-image` helper that pulls the deployed image
from the configured registry when it is missing locally, gated on
push-on-release being enabled, honoring per-app `registry:login` credentials.
Every in-tree `verify_image` caller migrates to the wrapper and the bare
helper is marked deprecated. `config:set` now forwards the deployed image tag
to `release-and-deploy` so the release path no longer pins recovery on the
unrecoverable `:latest` tag.
# History
## 0.38.6
Install/update via the bootstrap script:
```shell
wget -NP . https://dokku.com/install/v0.38.6/bootstrap.sh
sudo DOKKU_TAG=v0.38.6 bash bootstrap.sh
```
### Bug Fixes
- #8633: @Mordred Correctly redirect http traffic to https when using scheduler-k3s
- #8665: @josegonzalez Clarify byte-preserving behavior of config:set --encoded
- #8666: @josegonzalez Depend on cron | cron-daemon to allow alternatives
- #8638: @josegonzalez Support --global on cron:set
### Refactors
- #8668: @josegonzalez Hash scheduler-k3s cron-id label to fit Kubernetes' 63-byte cap
- #8664: @josegonzalez Convert filesystem migration markers to plugin properties
### Documentation
- #8645: @zenspider Removed outdated warning about dokku-update not being able to upgrade to specific versions.
### Tests
- #8667: @josegonzalez Rename logs:report vector key test and guard old keys
- #8662: @dependabot[bot] chore(deps): bump qs from 6.15.0 to 6.15.2 in /tests/apps/checks-root
- #8661: @dependabot[bot] chore(deps): bump sass from 1.99.0 to 1.100.0 in /tests/apps/multi
- #8658: @dependabot[bot] chore(deps-dev): bump heroku/heroku-buildpack-php from 288 to 290 in /tests/apps/php
- #8657: @dependabot[bot] chore(deps): bump slim/slim from 4.15.1 to 4.15.2 in /tests/apps/php
- #8654: @dependabot[bot] chore(deps): bump ruby from 4.0.4 to 4.0.5 in /tests/apps/dockerfile-entrypoint
- #8642: @dependabot[bot] chore(deps): bump google.golang.org/grpc from 1.81.0 to 1.81.1 in /tests/apps/gogrpc
### Dependencies
- #8652: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.40.0 to 1.41.0 in /plugins/common
- #8663: @dokku-bot chore: bump herokuish to 0.11.13
- #8646: @dokku-bot chore: bump pack to 0.40.6
- #8659: @dependabot[bot] chore(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0 in /plugins/common
- #8660: @dependabot[bot] chore(deps): bump click from 8.4.0 to 8.4.1 in /docs/_build
- #8655: @dependabot[bot] chore(deps): bump github.com/containerd/containerd from 1.7.30 to 1.7.32 in /plugins/scheduler-k3s
- #8651: @dependabot[bot] chore(deps): bump click from 8.3.3 to 8.4.0 in /docs/_build
- #8649: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.40.0 to 1.41.0 in /plugins/buildpacks
- #8650: @dependabot[bot] chore(deps): bump github.com/onsi/gomega from 1.40.0 to 1.41.0 in /plugins/config
- #8648: @dependabot[bot] chore(deps): bump zipp from 3.23.1 to 4.1.0 in /docs/_build
- #8644: @dokku-bot chore: bump pack to 0.40.5
- #8641: @dependabot[bot] chore(deps): bump traefik from v3.7.0 to v3.7.1 in /plugins/traefik-vhosts
- #8634: @dependabot[bot] chore(deps): bump pymdown-extensions from 10.21.2 to 10.21.3 in /docs/_build
### Other
- #8640: @josegonzalez Split report global keys into raw and computed
The docker-options DOCKER_OPTIONS_<PHASE>.migrated sentinels and the storage data/storage-registry/migrations/<app> flag files are now stored as per-app properties (`migrated-<phase>` and `legacy-mounts-migrated` respectively), so they survive a property-store backup/restore and align with the existing `migrated-from-files`, `env-migrated`, and `nginx-conf-sigil-migrated` patterns. The new markers are written only when a non-empty legacy source was actually migrated, distinguishing apps that never had legacy state from apps that did and were drained. A one-cycle upgrade-cycle conversion drains any leftover filesystem sentinels into the new properties and removes the files; for docker-options the conversion deliberately runs before the global `migrated-from-files` short-circuit so installs upgrading from the previous release still get their `.migrated` files converted.
# 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
`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.
# 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.
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.
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.
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.