v3.0.0-alpha.1 6fdddb8

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

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

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

[service.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.

gordon apps restart is separate from deploy and stays in place: it withdraws traffic, restarts the same 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:

[[service.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:

[[service.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:

[service.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. Secret updates affect the next deploy/restart, not running containers. 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 reload

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