v3.3.0 b164fe9

concepts.md RawHistoryFolder

Core Concepts

Understanding how Gordon works and why it's designed this way.

Local-First Development

Your development machine likely has 8-16 cores and 16-32GB RAM. Your VPS has 1-2 cores and 1-4GB RAM. Why build containers on the weak machine?

Gordon flips the typical deployment model:

  1. Build locally where you have computing power
  2. Push the finished image to your VPS registry
  3. Apply the app manifest declaring the image
  4. Deploy to activate it

This means faster builds, less VPS resource usage, and a simpler deployment workflow.

Push, Apply, Deploy

Gordon combines a Docker registry with a declarative app runtime:

┌──────────────┐      push       ┌──────────────┐
│ docker build │  ──────────────>│   Gordon     │
│ docker push  │   (OCI only,    │   Registry   │
└──────────────┘   never deploys)└──────┬───────┘
                                        │
                    ┌───────────────────┴───────────────────┐
                    │  gordon apps apply --file blog.toml   │  desired state
                    │  gordon apps deploy blog              │  activation
                    └───────────────────┬───────────────────┘
                                        v
                                 ┌──────────────┐
                                 │   App        │
                                 │  Containers  │
                                 └──────────────┘

Pushing an image only stores it. Nothing runs, no route is created, no manifest is modified. Activation is always an explicit gordon apps deploy.

Declarative Apps

One standalone TOML file defines one globally named app with one or more explicitly named image-backed services. The app owns its entrypoints and routes; containers are replaceable runtime instances, not public identities.

name = "blog"

[env]
APP_ENV = "production"   # app-wide public env, injected into all services

[services.web]
image = "gordon.mydomain.com/blog:1.4.2"

[[services.web.http]]
host = "blog.mydomain.com"
port = 3000

[services.web.env]            # public env for this service only
LOG_LEVEL = "info"

[services.web.secrets]        # ENV name -> secret name (values stay in pass)
DATABASE_URL = "database-url"
  • apps apply --file FILE validates and persists desired configuration only.
  • deploy APP activates it. apply --deploy chains both using exactly the revision accepted by apply.
  • --dry-run validates and previews without persistence or runtime effects.
  • Version tags are recommended, not constrained to SemVer. latest remains valid; explicit deploy re-resolves mutable tags while restart uses the active pinned content.
  • The file is intended for Git: it must never contain secret values. Confidential values use secrets; public values use [env] or [services.<name>.env].
  • Staging is an ordinary app in another TOML file. There is no pin, no preview environments, and no historical rollback command.

Updates

Gordon deploys an app's services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; every other service is replaced sequentially and may be briefly interrupted. Gordon never runs two Gordon-managed generations of a service that owns a volume, a bind, or a device, and a UDP service is excluded because UDP sessions stay pinned to the backend they started on.

Preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations). If preflight fails, the running service is left untouched.

A stateless service (no volume, bind, device, or UDP interface) is replaced with a short overlap:

Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old

The old container keeps serving and stays routed until the new one is ready and traffic points at it. Only then does the old container receive SIGTERM, with its own stop_grace (default 30s) before it is killed. While both run, they share the service's network alias, so a request from another service of the app may reach either generation. A database migration must therefore stay backward compatible with the previous version of the service. If the new container never becomes ready, it is removed and the old one is not touched. If the old container cannot be removed after the switch, the deploy still succeeds and reports a cleanup warning. Before the switch Gordon durably marks the old container for retirement, so the periodic recovery pass keeps retrying it, with the old container's stop_grace, once the new container is verified; later deploys do not lose the cleanup.

A service with a volume, a bind, a device, or a UDP interface is replaced sequentially:

Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic

If replacement fails after the old container was removed, the failure is explicit: Gordon does not recreate the old container during that operation and does not promise automatic data rollback. A stateful service's recovery inhibition stays in place until its never-published candidate is confirmed removed; a later boot/start/restart then clears it and rebuilds the generation ACTIVE records from its pinned digest, while a candidate that cannot be removed fails recovery closed.

Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. This covers TCP, UDP, mixed, and volume-owning services. An open UDP socket is not application readiness, and Gordon never restarts an old volume-owning image automatically after a replacement may have written data.

A deploy interrupted after the replacement container was created but before it was published leaves the failure explicit: the candidate is recorded in the deployment journal, and Gordon removes it before it creates or rebuilds any generation of that app — at boot and before every mutation. For a stateless service the old container keeps serving and is kept. A deploy interrupted after the switch but before the old container was stopped leaves two containers running; the retirement mark makes the periodic recovery pass stop the old one, but only once that same pass has verified and routed the published container. gordon apps stop and gordon apps remove also stop any such old container, and fail, keeping the mark, if they cannot. Boot recovery never routes a persisted port it has not verified. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. The next recovery pass republishes routing within its 15-second cadence if the final traffic publication was the step that failed.

Deploy is the only command that applies changes: it recreates a service whose image digest, spec, app environment, or secret values changed, and keeps an unchanged one, reported unchanged.

gordon apps restart restarts the same container and applies nothing: it withdraws traffic, restarts the pinned container, verifies readiness, and republishes traffic. No second container is created. When the recorded container is gone, restart rebuilds the service from its pinned ACTIVE digest and publishes it.

Deletion and Cleanup Lifecycle

Gordon separates workload removal from destructive data cleanup:

  • desired — the persisted manifest revision waiting to be activated.
  • active — the pinned, running definition (never inferred from containers).
  • stopped — durable stopped intent: workloads are down, data preserved, reboot keeps them stopped.
  • retained — volumes and secrets kept after app removal under the old internal UUID, visible but never implicitly adopted by a new app reusing the name.

Safe removal is the default. gordon apps remove withdraws workloads and frees the name; volumes and secrets are retained as owned orphans. There is deliberately no purge: destructive volume deletion requires a separately accepted destructive-action contract. Ordinary apply/deploy/restart/stop/remove never delete user volumes.

App HTTP Hosts

An app's HTTP interfaces declare the hosts Gordon serves:

[[services.web.http]]
host = "app.example.com"
port = 3000

When a request comes in for app.example.com, Gordon:

  1. Looks up the host in the ACTIVE projection (merged with installation external routes)
  2. Finds the recorded loopback backend for that host (never a container IP — rootless-first)
  3. Proxies the request to that backend

Hostnames must be plain hostnames. https behavior per host follows the tls mode (auto, always, never).

Networks

Each app gets a private network automatically. Services can additionally join named shared networks, created and reused only within verified Gordon ownership:

[[network.shared]]
network = "backend"
services = ["web", "worker"]

Deploy adds AND removes memberships without disconnecting unrelated services. Short DNS names resolve privately; app-qualified aliases apply on shared networks.

Volumes

Services declare persistent storage in the app manifest. Every Dockerfile VOLUME path must be declared explicitly; deployment rejects unmanaged image volumes:

[[services.web.volume]]
name = "web-data"
path = "/data"

Docker/Podman own the named volumes; Gordon records app UUID, service, and logical volume ownership. App manifests allow neither bind mounts nor service-shared volumes. Replacement and restart reuse volumes. Removing a service or app retains its volumes under the original app UUID, and a new app reusing the public name never adopts them.

Use gordon volumes prune --dry-run to inspect Gordon's ownership-aware plan. Do not use docker volume prune or an equivalent runtime command for Gordon data: it bypasses Gordon's retention checks and can delete unmounted retained volumes.

Environment and Secrets

App-wide public env is declared in the manifest:

[env]
APP_ENV = "production"

Public values for one service use [services.<name>.env] and override [env] keys for that service. Confidential values use secrets:

[services.web.env]
LOG_LEVEL = "info"

[services.web.secrets]
DATABASE_URL = "database-url"

Secret values stay in pass under gordon/apps/<uuid>/<service>/<name>, keyed by the stable internal UUID so a removed app's secrets are never adopted by a new app reusing the name. Running containers keep the values they were created with; gordon apps deploy applies new values. restart does not. Write values with gordon apps secrets set; only key names are ever echoed back, never values.

Installation Reload

Gordon watches gordon.toml and reloads installation-only settings (entrypoints, TLS, limits, external routes). Reload never activates pending app desired state, never re-resolves image tags, and never starts app workloads.

gordon daemon reload

gordon daemon reload sends SIGUSR1 to the running Gordon process. gordon.toml holds installation settings only — app workloads live in app files.

Event System

Gordon uses an internal event system for coordination. Registry storage events no longer deploy anything: push events never deploy, create routes, or modify manifests.

Backups and Recovery

Gordon runs PostgreSQL logical backups and volume archives to S3 for explicitly declared app targets. Declarations live in the app manifest ([[services.<name>.database]], [services.<name>.backup]); storage infrastructure (destinations, schedules, retention) stays global.

Stored backups are never deleted when declarations change — only schedules update on deploy.

For configuration details and usage examples, see the Backups Configuration guide, Backup CLI reference, and Configuration Reference.

Container Identity

Gordon stamps app ownership labels on every container and volume it creates:

Label Purpose
gordon.managed=true Identifies Gordon-managed resources
gordon.app App public name
gordon.app.service Service name
gordon.app.revision Active revision that created it

Queries by logical identity use labels, never name parsing. Unknown resources (no labels, old labels) are preserved, never adopted or deleted.

source
1# Core Concepts
2
3Understanding how Gordon works and why it's designed this way.
4
5## Local-First Development
6
7Your development machine likely has 8-16 cores and 16-32GB RAM. Your VPS has 1-2 cores and 1-4GB RAM. Why build containers on the weak machine?
8
9Gordon flips the typical deployment model:
10
111. **Build locally** where you have computing power
122. **Push the finished image** to your VPS registry
133. **Apply the app manifest** declaring the image
144. **Deploy** to activate it
15
16This means faster builds, less VPS resource usage, and a simpler deployment workflow.
17
18## Push, Apply, Deploy
19
20Gordon combines a Docker registry with a declarative app runtime:
21
22```
23┌──────────────┐      push       ┌──────────────┐
24│ docker build │  ──────────────>│   Gordon     │
25│ docker push  │   (OCI only,    │   Registry   │
26└──────────────┘   never deploys)└──────┬───────┘
27                                        │
28                    ┌───────────────────┴───────────────────┐
29                    │  gordon apps apply --file blog.toml   │  desired state
30                    │  gordon apps deploy blog              │  activation
31                    └───────────────────┬───────────────────┘
32                                        v
33                                 ┌──────────────┐
34                                 │   App        │
35                                 │  Containers  │
36                                 └──────────────┘
37```
38
39Pushing an image only stores it. Nothing runs, no route is created, no manifest is modified. Activation is always an explicit `gordon apps deploy`.
40
41## Declarative Apps
42
43One standalone TOML file defines one globally named app with one or more explicitly named image-backed services. The app owns its entrypoints and routes; containers are replaceable runtime instances, not public identities.
44
45```toml
46name = "blog"
47
48[env]
49APP_ENV = "production"   # app-wide public env, injected into all services
50
51[services.web]
52image = "gordon.mydomain.com/blog:1.4.2"
53
54[[services.web.http]]
55host = "blog.mydomain.com"
56port = 3000
57
58[services.web.env]            # public env for this service only
59LOG_LEVEL = "info"
60
61[services.web.secrets]        # ENV name -> secret name (values stay in pass)
62DATABASE_URL = "database-url"
63```
64
65- `apps apply --file FILE` validates and persists desired configuration only.
66- `deploy APP` activates it. `apply --deploy` chains both using exactly the revision accepted by apply.
67- `--dry-run` validates and previews without persistence or runtime effects.
68- Version tags are recommended, not constrained to SemVer. `latest` remains valid; explicit deploy re-resolves mutable tags while restart uses the active pinned content.
69- The file is intended for Git: it must never contain secret values. Confidential values use `secrets`; public values use `[env]` or `[services.<name>.env]`.
70- Staging is an ordinary app in another TOML file. There is no pin, no preview environments, and no historical rollback command.
71
72## Updates
73
74Gordon deploys an app's services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; every other service is replaced sequentially and may be briefly interrupted. Gordon never runs two Gordon-managed generations of a service that owns a volume, a bind, or a device, and a UDP service is excluded because UDP sessions stay pinned to the backend they started on.
75
76Preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations). If preflight fails, the running service is left untouched.
77
78A stateless service (no volume, bind, device, or UDP interface) is replaced with a short overlap:
79
80```
81Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old
82```
83
84The old container keeps serving and stays routed until the new one is ready and traffic points at it. Only then does the old container receive SIGTERM, with its own `stop_grace` (default 30s) before it is killed. While both run, they share the service's network alias, so a request from another service of the app may reach either generation. A database migration must therefore stay backward compatible with the previous version of the service. If the new container never becomes ready, it is removed and the old one is not touched. If the old container cannot be removed after the switch, the deploy still succeeds and reports a cleanup warning. Before the switch Gordon durably marks the old container for retirement, so the periodic recovery pass keeps retrying it, with the old container's `stop_grace`, once the new container is verified; later deploys do not lose the cleanup.
85
86A service with a volume, a bind, a device, or a UDP interface is replaced sequentially:
87
88```
89Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic
90```
91
92If replacement fails after the old container was removed, the failure is explicit: Gordon does not recreate the old container during that operation and does not promise automatic data rollback. A stateful service's recovery inhibition stays in place until its never-published candidate is confirmed removed; a later boot/start/restart then clears it and rebuilds the generation `ACTIVE` records from its pinned digest, while a candidate that cannot be removed fails recovery closed.
93
94Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. This covers TCP, UDP, mixed, and volume-owning services. An open UDP socket is not application readiness, and Gordon never restarts an old volume-owning image automatically after a replacement may have written data.
95
96A deploy interrupted after the replacement container was created but before it was published leaves the failure explicit: the candidate is recorded in the deployment journal, and Gordon removes it before it creates or rebuilds any generation of that app — at boot and before every mutation. For a stateless service the old container keeps serving and is kept. A deploy interrupted after the switch but before the old container was stopped leaves two containers running; the retirement mark makes the periodic recovery pass stop the old one, but only once that same pass has verified and routed the published container. `gordon apps stop` and `gordon apps remove` also stop any such old container, and fail, keeping the mark, if they cannot. Boot recovery never routes a persisted port it has not verified. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. The next recovery pass republishes routing within its 15-second cadence if the final traffic publication was the step that failed.
97
98Deploy is the only command that applies changes: it recreates a service whose image digest, spec, app environment, or secret values changed, and keeps an unchanged one, reported `unchanged`.
99
100`gordon apps restart` restarts the same container and applies nothing: it withdraws traffic, restarts the pinned container, verifies readiness, and republishes traffic. No second container is created. When the recorded container is gone, restart rebuilds the service from its pinned `ACTIVE` digest and publishes it.
101
102## Deletion and Cleanup Lifecycle
103
104Gordon separates workload removal from destructive data cleanup:
105
106- **desired** — the persisted manifest revision waiting to be activated.
107- **active** — the pinned, running definition (never inferred from containers).
108- **stopped** — durable stopped intent: workloads are down, data preserved, reboot keeps them stopped.
109- **retained** — volumes and secrets kept after app removal under the old internal UUID, visible but never implicitly adopted by a new app reusing the name.
110
111Safe removal is the default. `gordon apps remove` withdraws workloads and frees the name; volumes and secrets are retained as owned orphans. There is deliberately no `purge`: destructive volume deletion requires a separately accepted destructive-action contract. Ordinary apply/deploy/restart/stop/remove never delete user volumes.
112
113## App HTTP Hosts
114
115An app's HTTP interfaces declare the hosts Gordon serves:
116
117```toml
118[[services.web.http]]
119host = "app.example.com"
120port = 3000
121```
122
123When a request comes in for `app.example.com`, Gordon:
124
1251. Looks up the host in the ACTIVE projection (merged with installation external routes)
1262. Finds the recorded loopback backend for that host (never a container IP — rootless-first)
1273. Proxies the request to that backend
128
129Hostnames must be plain hostnames. `https` behavior per host follows the `tls` mode (`auto`, `always`, `never`).
130
131## Networks
132
133Each app gets a private network automatically. Services can additionally join named shared networks, created and reused only within verified Gordon ownership:
134
135```toml
136[[network.shared]]
137network = "backend"
138services = ["web", "worker"]
139```
140
141Deploy adds AND removes memberships without disconnecting unrelated services. Short DNS names resolve privately; app-qualified aliases apply on shared networks.
142
143## Volumes
144
145Services declare persistent storage in the app manifest. Every Dockerfile `VOLUME` path must be declared explicitly; deployment rejects unmanaged image volumes:
146
147```toml
148[[services.web.volume]]
149name = "web-data"
150path = "/data"
151```
152
153Docker/Podman own the named volumes; Gordon records app UUID, service, and logical volume ownership. App manifests allow neither bind mounts nor service-shared volumes. Replacement and restart reuse volumes. Removing a service or app retains its volumes under the original app UUID, and a new app reusing the public name never adopts them.
154
155Use `gordon volumes prune --dry-run` to inspect Gordon's ownership-aware plan. Do not use `docker volume prune` or an equivalent runtime command for Gordon data: it bypasses Gordon's retention checks and can delete unmounted retained volumes.
156
157## Environment and Secrets
158
159App-wide public env is declared in the manifest:
160
161```toml
162[env]
163APP_ENV = "production"
164```
165
166Public values for one service use `[services.<name>.env]` and override `[env]` keys for that service. Confidential values use `secrets`:
167
168```toml
169[services.web.env]
170LOG_LEVEL = "info"
171
172[services.web.secrets]
173DATABASE_URL = "database-url"
174```
175
176Secret values stay in pass under `gordon/apps/<uuid>/<service>/<name>`, keyed by the stable internal UUID so a removed app's secrets are never adopted by a new app reusing the name. Running containers keep the values they were created with; `gordon apps deploy` applies new values. `restart` does not. Write values with `gordon apps secrets set`; only key names are ever echoed back, never values.
177
178## Installation Reload
179
180Gordon watches `gordon.toml` and reloads installation-only settings (entrypoints, TLS, limits, external routes). Reload never activates pending app desired state, never re-resolves image tags, and never starts app workloads.
181
182```bash
183gordon daemon reload
184```
185
186`gordon daemon reload` sends `SIGUSR1` to the running Gordon process. `gordon.toml` holds installation settings only — app workloads live in app files.
187
188## Event System
189
190Gordon uses an internal event system for coordination. Registry storage events no longer deploy anything: push events never deploy, create routes, or modify manifests.
191
192## Backups and Recovery
193
194Gordon runs PostgreSQL logical backups and volume archives to S3 for explicitly declared app targets. Declarations live in the app manifest (`[[services.<name>.database]]`, `[services.<name>.backup]`); storage infrastructure (destinations, schedules, retention) stays global.
195
196Stored backups are never deleted when declarations change — only schedules update on deploy.
197
198For configuration details and usage examples, see the [Backups Configuration guide](./config/backups.md), [Backup CLI reference](./cli/backup.md), and [Configuration Reference](./config/reference.md).
199
200## Container Identity
201
202Gordon stamps app ownership labels on every container and volume it creates:
203
204| Label | Purpose |
205|-------|---------|
206| `gordon.managed=true` | Identifies Gordon-managed resources |
207| `gordon.app` | App public name |
208| `gordon.app.service` | Service name |
209| `gordon.app.revision` | Active revision that created it |
210
211Queries by logical identity use labels, never name parsing. Unknown resources (no labels, old labels) are preserved, never adopted or deleted.
212
213## Related
214
215- [Configuration Reference](./config/index.md)
216- [Apps CLI](./cli/apps.md)
217- [Getting Started](./getting-started.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