A workload bind mount accepts two forms, and they have materially different lifetimes. The contract documents neither the difference nor the second form.
The lifetime difference
internal/app/generate.go:318 passes Volume.Source through verbatim, and the release runs as docker compose -p <app> -f <release>/compose.yaml with no --project-directory (internal/engine/roll.go:16). Compose resolves a relative host path against the directory holding the compose file, which is the release directory.
So {source: ./data, path: /data} is storage that lives inside one release. pruneRetention (internal/engine/deploy.go:434) removes release directories once they fall outside deployment.retain_releases, so with the default retention that mount and everything written to it is deleted on the fifth subsequent deploy. {source: /srv/app/data, path: /data} is stable host state that no deploy touches.
One character of difference in the same field, and the consequence is silent data loss on one side of it. Nothing in the schema, the field description, or the docs mentions this.
The field description is also wrong
internal/app/types.go:224 reads:
Source string `json:"source,omitempty" description:"Repository-relative source path of a bind mount." example:"./config"`
Absolute host paths are supported and exercised by this repository:
internal/app/testdata/corpus/ext-authentik.yml:25 — {source: /var/run/docker.sock, path: /var/run/docker.sock}
e2e/apps/authentik.yml:20-33 — /srv/authentik/media, /srv/authentik/certs, /srv/authentik/templates
internal/app/generate_test.go:44 — {source: /data/postgres, path: /var/lib/postgresql/data}
The same corpus file uses both forms, so this is a supported pair, not a tolerated accident. git log -L 224,224:internal/app/types.go shows the field shipped without a description in f05f870 and gained this one in f6e6495 ("docs: harden CLI and spec documentation") — a documentation pass that described one form and did not mention the other.
The practical cost is that a reader following the schema concludes an absolute source is illegal, and a reader who guesses the relative form gets release-scoped storage without being told.
Suggested fix
Describe both forms and the lifetime that follows from each — something like: "Bind mount source. An absolute path is host state that outlives every release. A relative path resolves inside the release directory and is removed with that release by deployment.retain_releases." Examples should show both. If release-scoped storage is not a use case worth supporting, refusing a relative source with that reason would be clearer than documenting it.
Found while migrating an application from a container cron runner to role: job, whose Docker socket mount is one of the absolute-source cases.
A workload bind mount accepts two forms, and they have materially different lifetimes. The contract documents neither the difference nor the second form.
The lifetime difference
internal/app/generate.go:318passesVolume.Sourcethrough verbatim, and the release runs asdocker compose -p <app> -f <release>/compose.yamlwith no--project-directory(internal/engine/roll.go:16). Compose resolves a relative host path against the directory holding the compose file, which is the release directory.So
{source: ./data, path: /data}is storage that lives inside one release.pruneRetention(internal/engine/deploy.go:434) removes release directories once they fall outsidedeployment.retain_releases, so with the default retention that mount and everything written to it is deleted on the fifth subsequent deploy.{source: /srv/app/data, path: /data}is stable host state that no deploy touches.One character of difference in the same field, and the consequence is silent data loss on one side of it. Nothing in the schema, the field description, or the docs mentions this.
The field description is also wrong
internal/app/types.go:224reads:Absolute host paths are supported and exercised by this repository:
internal/app/testdata/corpus/ext-authentik.yml:25—{source: /var/run/docker.sock, path: /var/run/docker.sock}e2e/apps/authentik.yml:20-33—/srv/authentik/media,/srv/authentik/certs,/srv/authentik/templatesinternal/app/generate_test.go:44—{source: /data/postgres, path: /var/lib/postgresql/data}The same corpus file uses both forms, so this is a supported pair, not a tolerated accident.
git log -L 224,224:internal/app/types.goshows the field shipped without a description in f05f870 and gained this one in f6e6495 ("docs: harden CLI and spec documentation") — a documentation pass that described one form and did not mention the other.The practical cost is that a reader following the schema concludes an absolute source is illegal, and a reader who guesses the relative form gets release-scoped storage without being told.
Suggested fix
Describe both forms and the lifetime that follows from each — something like: "Bind mount source. An absolute path is host state that outlives every release. A relative path resolves inside the release directory and is removed with that release by
deployment.retain_releases." Examples should show both. If release-scoped storage is not a use case worth supporting, refusing a relative source with that reason would be clearer than documenting it.Found while migrating an application from a container cron runner to
role: job, whose Docker socket mount is one of the absolute-source cases.