v3.0.0 f19f453

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.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. Service-specific values use secrets even when non-confidential.
  • 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 deploy may cause a short service interruption; there is no zero-downtime promise for app services. Gordon never runs two Gordon-managed generations of the same service at the same time.

Every replaced service follows the same flow: preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations); the service is withdrawn from traffic; the old container is stopped and removed; the new container is created and started; the declared readiness probe runs; the new ACTIVE state is published; and traffic is rebuilt and published for the new container.

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

If preflight fails, the running service is left untouched. 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 flow covers every service, including 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 — so two generations of one service never run together. 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 the 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"

Service-specific values use secrets even when non-confidential:

[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.secrets]        # ENV name -> secret name (values stay in pass)
59DATABASE_URL = "database-url"
60```
61
62- `apps apply --file FILE` validates and persists desired configuration only.
63- `deploy APP` activates it. `apply --deploy` chains both using exactly the revision accepted by apply.
64- `--dry-run` validates and previews without persistence or runtime effects.
65- Version tags are recommended, not constrained to SemVer. `latest` remains valid; explicit deploy re-resolves mutable tags while restart uses the active pinned content.
66- The file is intended for Git: it must never contain secret values. Service-specific values use `secrets` even when non-confidential.
67- Staging is an ordinary app in another TOML file. There is no pin, no preview environments, and no historical rollback command.
68
69## Updates
70
71Gordon deploys an app's services one at a time in sorted order. A deploy may cause a short service interruption; there is no zero-downtime promise for app services. Gordon never runs two Gordon-managed generations of the same service at the same time.
72
73Every replaced service follows the same flow: preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations); the service is withdrawn from traffic; the old container is stopped and removed; the new container is created and started; the declared readiness probe runs; the new `ACTIVE` state is published; and traffic is rebuilt and published for the new container.
74
75```
76Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic
77```
78
79If preflight fails, the running service is left untouched. 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.
80
81Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. This flow covers every service, including 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.
82
83A 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 — so two generations of one service never run together. 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 the 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.
84
85Deploy 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`.
86
87`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.
88
89## Deletion and Cleanup Lifecycle
90
91Gordon separates workload removal from destructive data cleanup:
92
93- **desired** — the persisted manifest revision waiting to be activated.
94- **active** — the pinned, running definition (never inferred from containers).
95- **stopped** — durable stopped intent: workloads are down, data preserved, reboot keeps them stopped.
96- **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.
97
98Safe 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.
99
100## App HTTP Hosts
101
102An app's HTTP interfaces declare the hosts Gordon serves:
103
104```toml
105[[services.web.http]]
106host = "app.example.com"
107port = 3000
108```
109
110When a request comes in for `app.example.com`, Gordon:
111
1121. Looks up the host in the ACTIVE projection (merged with installation external routes)
1132. Finds the recorded loopback backend for that host (never a container IP — rootless-first)
1143. Proxies the request to that backend
115
116Hostnames must be plain hostnames. `https` behavior per host follows the `tls` mode (`auto`, `always`, `never`).
117
118## Networks
119
120Each app gets a private network automatically. Services can additionally join named shared networks, created and reused only within verified Gordon ownership:
121
122```toml
123[[network.shared]]
124network = "backend"
125services = ["web", "worker"]
126```
127
128Deploy adds AND removes memberships without disconnecting unrelated services. Short DNS names resolve privately; app-qualified aliases apply on shared networks.
129
130## Volumes
131
132Services declare persistent storage in the app manifest. Every Dockerfile `VOLUME` path must be declared explicitly; deployment rejects unmanaged image volumes:
133
134```toml
135[[services.web.volume]]
136name = "web-data"
137path = "/data"
138```
139
140Docker/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.
141
142Use `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.
143
144## Environment and Secrets
145
146App-wide public env is declared in the manifest:
147
148```toml
149[env]
150APP_ENV = "production"
151```
152
153Service-specific values use `secrets` even when non-confidential:
154
155```toml
156[services.web.secrets]
157DATABASE_URL = "database-url"
158```
159
160Secret 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.
161
162## Installation Reload
163
164Gordon 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.
165
166```bash
167gordon daemon reload
168```
169
170`gordon daemon reload` sends `SIGUSR1` to the running Gordon process. `gordon.toml` holds installation settings only — app workloads live in app files.
171
172## Event System
173
174Gordon uses an internal event system for coordination. Registry storage events no longer deploy anything: push events never deploy, create routes, or modify manifests.
175
176## Backups and Recovery
177
178Gordon 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.
179
180Stored backups are never deleted when declarations change — only schedules update on deploy.
181
182For 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).
183
184## Container Identity
185
186Gordon stamps app ownership labels on every container and volume it creates:
187
188| Label | Purpose |
189|-------|---------|
190| `gordon.managed=true` | Identifies Gordon-managed resources |
191| `gordon.app` | App public name |
192| `gordon.app.service` | Service name |
193| `gordon.app.revision` | Active revision that created it |
194
195Queries by logical identity use labels, never name parsing. Unknown resources (no labels, old labels) are preserved, never adopted or deleted.
196
197## Related
198
199- [Configuration Reference](./config/index.md)
200- [Apps CLI](./cli/apps.md)
201- [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