main bac21c6

volumes.md RawHistoryFolder

Volumes Configuration

Persistent app storage is declared in each app manifest. The installation-level [volumes] settings apply to non-app volume management and do not control declarative app storage.

Declarative app volumes

Services declare persistent mounts in the app manifest:

[[services.web.volume]]
name = "database-data"
path = "/var/lib/postgresql/data"

A volume name is unique within its service. Gordon creates an incarnation-owned runtime volume, records the app, app UUID, service, and logical volume ownership, and reuses it across replacement and restart. Manifests never carry host paths, and sharing one volume between services is not supported. When an app must read an operator-approved host location, use a named administrative bind instead.

Administrative bind mounts

Manifests cannot name host paths directly. The operator declares a named policy in gordon.toml; the manifest only references the policy name:

# gordon.toml
[app_mounts.app-logs]
source = "/srv/gordon/host-logs"   # required; the only host path this mount may come from
read_only = true
allowed_apps = ["metrics-agent"]   # required; exact, non-empty
allowed_services = ["web"]         # required; exact, non-empty
# root = "/srv/gordon"             # optional administrative boundary
# app manifest
[[services.web.bind]]
name = "app-logs"        # must match [app_mounts.<name>]
path = "/var/lib/collector/host-logs"   # container destination
readonly = true
  • source must be absolute, normalized, and resolve through symlinks to a regular file or directory that stays under root. When root is omitted, the source's parent directory is the boundary, so a symlinked source may resolve within it but never escape it.
  • allowed_apps and allowed_services are exact, non-empty allowlists; there is no wildcard, prefix, or empty-means-all form.
  • Read-only precedence: read_only on the policy or readonly on the bind forces the resolved mount read-only. A bind never weakens its policy.
  • Gordon re-resolves the policy immediately before every container create, restart, and recovery. Removing a policy or an allowlist entry blocks future deploys; running containers keep their current mounts until redeployed. gordon serve reload republishes validated policies atomically, and a failing edit keeps the previous policies live.
  • Gordon never creates, copies, deletes, chowns, backs up, or prunes the host path; ownership and permissions stay with the operator.

A read-only log collector is the typical use. Its own state is a named volume; the host logs are exposed through a bind:

name = "metrics-agent"

[services.collector]
image = "registry.example.com/metrics/collector:2.1.0"

[[services.collector.volume]]
name = "collector-state"
path = "/var/lib/collector"

[[services.collector.bind]]
name = "app-logs"
path = "/var/lib/collector/host-logs"
readonly = true

Collection is one-way: the collector reads operator-owned logs and cannot write back through the bind.

Every Dockerfile VOLUME path must be mapped explicitly. A matching [[services.<name>.volume]] declaration maps it to a Gordon-owned volume; a [[services.<name>.bind]] whose container path equals the VOLUME path also satisfies the mapping, because the bind mounts over that image-declared volume. Any VOLUME path with neither mapping fails closed at deploy with an unmanaged-volume error. A bind destination may not collide with a declared volume path.

Runtime volume names are implementation details. Use gordon volumes list and ownership labels to inspect them; do not derive ownership from a name, rename volumes, or edit Gordon's ownership records.

Retention

Ordinary app operations retain data:

  • deploy and restart reuse the service's volumes;
  • removing a service retains its volumes;
  • gordon apps remove retains volumes under the removed app's internal UUID;
  • a new app that reuses the public name does not adopt retained volumes;
  • administrative binds are unaffected by app operations: their host paths are never created, adopted, or retained by Gordon.

Gordon does not automatically delete retained app volumes. Back up persistent data before any manual deletion, and use database-native backup and restore procedures for databases.

Manual migration

Gordon does not copy data between volumes. Stop every container that can write to the source, verify a backup, create an empty target volume, and perform the copy through the runtime so rootless UID/GID mappings are preserved. For Podman, a disposable helper container is the preferred generic method:

podman run --rm --pull=never --network=none \
  --volume "<source-volume>:/source:ro" \
  --volume "<target-volume>:/target" \
  docker.io/library/alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc \
  sh -c 'cp -a /source/. /target/'

Replace the literal <...> placeholders before running; they are quoted above so a shell cannot mistake them for redirection. The approved helper image is digest-pinned, must already be available locally, and runs without network access. If direct access to storage paths is unavoidable with rootless Podman, enter its user namespace:

podman unshare cp -a "<source-path>"/. "<target-path>"/

Do not use a plain root cp as the default procedure: host ownership IDs may not match the container's user namespace. Applications that rely on ACLs, extended attributes, sparse files, or database consistency need an application-specific export/restore or copy tool. Validate ownership and application behavior against the target, and retain the source volume until the migration is accepted.

Pruning

Use Gordon's ownership-aware command:

gordon volumes prune --dry-run
gordon volumes prune

A volume is eligible only when all of these requirements hold:

  1. Gordon has a durable ownership record marking it released.
  2. Runtime labels agree with the record's app, app incarnation UUID, and service.
  3. No container mounts the volume.

Retained, attached, unknown, contradictory, and unrelated volumes survive. A gordon.managed=true label or a matching name is not sufficient deletion authority. Administrative binds are out of scope entirely: they are operator-owned host paths, no ownership record exists for them, and they are never candidates. With current lifecycle metadata, no operation marks app volumes released, so prune normally succeeds with no deletions.

Warning: Do not use docker volume prune, podman volume prune, or equivalent runtime cleanup for Gordon data. Those commands bypass Gordon's ownership and retention checks and can delete an unmounted retained volume.

See the Volumes CLI reference for flags and plan output.

source
1# Volumes Configuration
2
3Persistent app storage is declared in each app manifest. The installation-level `[volumes]` settings apply to non-app volume management and do not control declarative app storage.
4
5## Declarative app volumes
6
7Services declare persistent mounts in the app manifest:
8
9```toml
10[[services.web.volume]]
11name = "database-data"
12path = "/var/lib/postgresql/data"
13```
14
15A volume name is unique within its service. Gordon creates an incarnation-owned runtime volume, records the app, app UUID, service, and logical volume ownership, and reuses it across replacement and restart. Manifests never carry host paths, and sharing one volume between services is not supported. When an app must read an operator-approved host location, use a named administrative bind instead.
16
17## Administrative bind mounts
18
19Manifests cannot name host paths directly. The operator declares a named policy in `gordon.toml`; the manifest only references the policy name:
20
21```toml
22# gordon.toml
23[app_mounts.app-logs]
24source = "/srv/gordon/host-logs"   # required; the only host path this mount may come from
25read_only = true
26allowed_apps = ["metrics-agent"]   # required; exact, non-empty
27allowed_services = ["web"]         # required; exact, non-empty
28# root = "/srv/gordon"             # optional administrative boundary
29```
30
31```toml
32# app manifest
33[[services.web.bind]]
34name = "app-logs"        # must match [app_mounts.<name>]
35path = "/var/lib/collector/host-logs"   # container destination
36readonly = true
37```
38
39- `source` must be absolute, normalized, and resolve through symlinks to a regular file or directory that stays under `root`. When `root` is omitted, the source's parent directory is the boundary, so a symlinked source may resolve within it but never escape it.
40- `allowed_apps` and `allowed_services` are exact, non-empty allowlists; there is no wildcard, prefix, or empty-means-all form.
41- Read-only precedence: `read_only` on the policy or `readonly` on the bind forces the resolved mount read-only. A bind never weakens its policy.
42- Gordon re-resolves the policy immediately before every container create, restart, and recovery. Removing a policy or an allowlist entry blocks future deploys; running containers keep their current mounts until redeployed. `gordon serve` reload republishes validated policies atomically, and a failing edit keeps the previous policies live.
43- Gordon never creates, copies, deletes, chowns, backs up, or prunes the host path; ownership and permissions stay with the operator.
44
45A read-only log collector is the typical use. Its own state is a named volume; the host logs are exposed through a bind:
46
47```toml
48name = "metrics-agent"
49
50[services.collector]
51image = "registry.example.com/metrics/collector:2.1.0"
52
53[[services.collector.volume]]
54name = "collector-state"
55path = "/var/lib/collector"
56
57[[services.collector.bind]]
58name = "app-logs"
59path = "/var/lib/collector/host-logs"
60readonly = true
61```
62
63Collection is one-way: the collector reads operator-owned logs and cannot write back through the bind.
64
65Every Dockerfile `VOLUME` path must be mapped explicitly. A matching `[[services.<name>.volume]]` declaration maps it to a Gordon-owned volume; a `[[services.<name>.bind]]` whose container `path` equals the `VOLUME` path also satisfies the mapping, because the bind mounts over that image-declared volume. Any `VOLUME` path with neither mapping fails closed at deploy with an unmanaged-volume error. A bind destination may not collide with a declared volume path.
66
67Runtime volume names are implementation details. Use `gordon volumes list` and ownership labels to inspect them; do not derive ownership from a name, rename volumes, or edit Gordon's ownership records.
68
69## Retention
70
71Ordinary app operations retain data:
72
73- deploy and restart reuse the service's volumes;
74- removing a service retains its volumes;
75- `gordon apps remove` retains volumes under the removed app's internal UUID;
76- a new app that reuses the public name does not adopt retained volumes;
77- administrative binds are unaffected by app operations: their host paths are never created, adopted, or retained by Gordon.
78
79Gordon does not automatically delete retained app volumes. Back up persistent data before any manual deletion, and use database-native backup and restore procedures for databases.
80
81## Manual migration
82
83Gordon does not copy data between volumes. Stop every container that can write to the source, verify a backup, create an empty target volume, and perform the copy through the runtime so rootless UID/GID mappings are preserved. For Podman, a disposable helper container is the preferred generic method:
84
85```bash
86podman run --rm --pull=never --network=none \
87  --volume "<source-volume>:/source:ro" \
88  --volume "<target-volume>:/target" \
89  docker.io/library/alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc \
90  sh -c 'cp -a /source/. /target/'
91```
92
93Replace the literal `<...>` placeholders before running; they are quoted above so a shell cannot mistake them for redirection. The approved helper image is digest-pinned, must already be available locally, and runs without network access. If direct access to storage paths is unavoidable with rootless Podman, enter its user namespace:
94
95```bash
96podman unshare cp -a "<source-path>"/. "<target-path>"/
97```
98
99Do not use a plain root `cp` as the default procedure: host ownership IDs may not match the container's user namespace. Applications that rely on ACLs, extended attributes, sparse files, or database consistency need an application-specific export/restore or copy tool. Validate ownership and application behavior against the target, and retain the source volume until the migration is accepted.
100
101## Pruning
102
103Use Gordon's ownership-aware command:
104
105```bash
106gordon volumes prune --dry-run
107gordon volumes prune
108```
109
110A volume is eligible only when all of these requirements hold:
111
1121. Gordon has a durable ownership record marking it `released`.
1132. Runtime labels agree with the record's app, app incarnation UUID, and service.
1143. No container mounts the volume.
115
116Retained, attached, unknown, contradictory, and unrelated volumes survive. A `gordon.managed=true` label or a matching name is not sufficient deletion authority. Administrative binds are out of scope entirely: they are operator-owned host paths, no ownership record exists for them, and they are never candidates. With current lifecycle metadata, no operation marks app volumes `released`, so prune normally succeeds with no deletions.
117
118> **Warning:** Do not use `docker volume prune`, `podman volume prune`, or equivalent runtime cleanup for Gordon data. Those commands bypass Gordon's ownership and retention checks and can delete an unmounted retained volume.
119
120See the [Volumes CLI reference](../cli/volumes.md) for flags and plan output.
121
122## Related
123
124- [App Manifest](./apps.md)
125- [Volumes CLI](../cli/volumes.md)
126- [Configuration Overview](./index.md)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close