Skip to content

volumes: a relative bind source is release-scoped storage, and the field says nothing about it #97

Description

@vishr

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions