v2.31.0-dev16 8d0a092

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
  3. Gordon deploys automatically

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

Push-to-Deploy

Gordon combines a Docker registry with automatic deployment:

┌──────────────┐      push       ┌──────────────┐
│ docker build │  ──────────────>│   Gordon     │
│ docker push  │                 │   Registry   │
└──────────────┘                 └──────┬───────┘
                                        │
                                        │ event: image.pushed
                                        v
                                 ┌──────────────┐
                                 │   Deploy     │
                                 │   Container  │
                                 └──────────────┘

When you push an image, Gordon:

  1. Stores the image in its registry
  2. Fires an image.pushed event
  3. Looks up the route for that image
  4. Deploys a new container
  5. Updates the proxy routing
  6. Stops the old container

Zero-Downtime Updates

Gordon ensures your app stays available during updates:

  1. New container starts while old container is still running
  2. Health check waits for new container to be ready
  3. Traffic switches to the new container
  4. Old container stops after traffic has moved
Time ─────────────────────────────────────────────>

Old Container:  [═══════════════════]
                                    ↓ stop
New Container:           [═════════════════════════>
                         ↑ start    ↑ traffic routed

Routes

Routes map domains to container images:

[routes]
"app.example.com" = { image = "myapp:latest" }
"api.example.com" = { image = "myapi:v2.1.0" }

Route domains must be plain hostnames such as app.example.com. Gordon rejects http:// and https:// prefixes, .local and .internal suffixes, localhost names, and IP literals. Legacy http://... keys are still read for backward compatibility and rewritten on the next save.

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

  1. Looks up the route configuration
  2. Finds the running container for myapp:latest
  3. Proxies the request to that container

Route Domains

Routes use plain hostnames. HTTPS is enabled by default for routes; see the routes example with dev-app.example.com. Cloudflare is one deployment option, but Gordon can also terminate TLS with its built-in listener and certificates issued by Gordon's internal CA, with static certificates, or with public ACME certificates when [tls.acme] is enabled. ACME supports HTTP-01 and Cloudflare DNS-01 challenge modes.

[routes]
"dev-app.example.com" = { image = "internal-app:latest", https = false }

Network Isolation

Each app runs in its own isolated Docker network:

┌────────────────────────────────────────────────┐
│ gordon-app-mydomain-com                        │
│                                                │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │   App    │───>│ Postgres │    │  Redis   │  │
│  │ :3000    │    │ :5432    │    │ :6379    │  │
│  └──────────┘    └──────────┘    └──────────┘  │
│                                                │
└────────────────────────────────────────────────┘

Benefits:

  • Containers can't access each other's services
  • Services are only accessible by name within their network
  • No port conflicts between apps

Attachments

Attachments are service dependencies for your apps:

[attachments]
"app.mydomain.com" = ["postgres:latest", "redis:latest"]

Gordon deploys attachments to the same network as your app. Services are accessible by their image name:

// In your app
const db = await connect("postgresql://postgres:5432/mydb");
const cache = await connect("redis://redis:6379");

Network Groups

Network groups allow multiple apps to share services:

[network_groups]
"backend" = ["app.mydomain.com", "api.mydomain.com"]

[attachments]
"backend" = ["shared-postgres:latest", "shared-redis:latest"]

Both app.mydomain.com and api.mydomain.com can access the shared services.

Volumes

Gordon automatically creates persistent storage from Dockerfile VOLUME directives:

FROM postgres:18
VOLUME ["/var/lib/postgresql/data"]

Volume behavior:

  • auto_create: Volumes are created automatically (default: true)
  • prefix: Volume names are prefixed with gordon- (configurable)
  • preserve: Volumes persist across container updates (default: true)

Environment Variables

Gordon loads environment variables from files based on the domain:

~/.gordon/env/
├── app_mydomain_com.env
├── api_mydomain_com.env
└── admin_mydomain_com.env

Domain dots become underscores: app.mydomain.com → app_mydomain_com.env

Variables are merged in order:

  1. Dockerfile ENV directives (lowest priority)
  2. .env file values (highest priority)

Secret Providers

Environment files support secret provider syntax:

# From Unix password manager (pass)
DATABASE_PASSWORD=${pass:myapp/db-password}

# From SOPS encrypted files
API_SECRET=${sops:secrets.yaml:api.secret}

Configuration Hot-Reload

Gordon watches its config file and reloads automatically:

  1. Edit ~/.config/gordon/gordon.toml
  2. Save the file
  3. Gordon reloads hot-reloaded settings such as attachments, network groups, and routes
  4. Containers and proxy config sync to match the new configuration

Route file edits now reload automatically again.

You can also trigger a reload:

gordon reload

gordon reload sends SIGUSR1 to the running Gordon process.

Event System

Gordon uses an internal event system for coordination:

Event Trigger Action
image.pushed Image pushed to registry Deploy container
config.reload Config file changed or gordon reload sends SIGUSR1 Reload config, sync containers, and refresh proxy state
manual.deploy gordon deploy <domain> command Deploy specific route
container.deployed Container started Update proxy cache

Backups and Recovery

Gordon can run logical PostgreSQL backups for attachment containers.

Current design:

  1. Detect PostgreSQL attachments attached to a route
  2. Execute pg_dump -Fc through Gordon runtime operations
  3. Store backup artifacts on local filesystem storage
  4. Expose backup actions through admin API and CLI

This is intentionally scoped for operational safety and predictable behavior. Future extensions can add physical backups, PITR, and remote object storage adapters. For configuration details and usage examples, see the Backups Configuration guide, Backup CLI reference, and Configuration Reference.

Container Labels

Gordon uses labels to track managed containers:

Label Purpose
gordon.managed=true Identifies Gordon-managed containers
gordon.domain Domain this container serves
gordon.image Image name and tag
gordon.route Route this container handles
gordon.attachment=true Container is an attachment service
gordon.attached-to Which route this attachment serves

Proxy Port Selection

When a container exposes multiple ports, Gordon needs to know which one serves HTTP:

FROM gitea/gitea:latest
LABEL gordon.proxy.port=3000  # Route HTTP to port 3000
EXPOSE 22   # SSH
EXPOSE 3000 # HTTP

Without the label, Gordon uses the first exposed port.

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
133. **Gordon deploys automatically**
14
15This means faster builds, less VPS resource usage, and a simpler deployment workflow.
16
17## Push-to-Deploy
18
19Gordon combines a Docker registry with automatic deployment:
20
21```
22┌──────────────┐      push       ┌──────────────┐
23│ docker build │  ──────────────>│   Gordon     │
24│ docker push  │                 │   Registry   │
25└──────────────┘                 └──────┬───────┘
26                                        │
27                                        │ event: image.pushed
28                                        v
29                                 ┌──────────────┐
30                                 │   Deploy     │
31                                 │   Container  │
32                                 └──────────────┘
33```
34
35When you push an image, Gordon:
36
371. Stores the image in its registry
382. Fires an `image.pushed` event
393. Looks up the route for that image
404. Deploys a new container
415. Updates the proxy routing
426. Stops the old container
43
44## Zero-Downtime Updates
45
46Gordon ensures your app stays available during updates:
47
481. **New container starts** while old container is still running
492. **Health check** waits for new container to be ready
503. **Traffic switches** to the new container
514. **Old container stops** after traffic has moved
52
53```
54Time ─────────────────────────────────────────────>
55
56Old Container:  [═══════════════════]
57                                    ↓ stop
58New Container:           [═════════════════════════>
59                         ↑ start    ↑ traffic routed
60```
61
62## Routes
63
64Routes map domains to container images:
65
66```toml
67[routes]
68"app.example.com" = { image = "myapp:latest" }
69"api.example.com" = { image = "myapi:v2.1.0" }
70```
71
72Route domains must be plain hostnames such as `app.example.com`. Gordon rejects `http://` and `https://` prefixes, `.local` and `.internal` suffixes, localhost names, and IP literals. Legacy `http://...` keys are still read for backward compatibility and rewritten on the next save.
73
74When a request comes in for `app.example.com`, Gordon:
75
761. Looks up the route configuration
772. Finds the running container for `myapp:latest`
783. Proxies the request to that container
79
80### Route Domains
81
82Routes use plain hostnames. HTTPS is enabled by default for routes; see the [routes example](./config/routes.md#development-setup) with `dev-app.example.com`. Cloudflare is one deployment option, but Gordon can also terminate TLS with its built-in listener and certificates issued by Gordon's internal CA, with static certificates, or with public ACME certificates when `[tls.acme]` is enabled. ACME supports HTTP-01 and Cloudflare DNS-01 challenge modes.
83
84```toml
85[routes]
86"dev-app.example.com" = { image = "internal-app:latest", https = false }
87```
88
89## Network Isolation
90
91Each app runs in its own isolated Docker network:
92
93```
94┌────────────────────────────────────────────────┐
95│ gordon-app-mydomain-com                        │
96│                                                │
97│  ┌──────────┐    ┌──────────┐    ┌──────────┐  │
98│  │   App    │───>│ Postgres │    │  Redis   │  │
99│  │ :3000    │    │ :5432    │    │ :6379    │  │
100│  └──────────┘    └──────────┘    └──────────┘  │
101│                                                │
102└────────────────────────────────────────────────┘
103```
104
105Benefits:
106
107- Containers can't access each other's services
108- Services are only accessible by name within their network
109- No port conflicts between apps
110
111## Attachments
112
113Attachments are service dependencies for your apps:
114
115```toml
116[attachments]
117"app.mydomain.com" = ["postgres:latest", "redis:latest"]
118```
119
120Gordon deploys attachments to the same network as your app. Services are accessible by their image name:
121
122```javascript
123// In your app
124const db = await connect("postgresql://postgres:5432/mydb");
125const cache = await connect("redis://redis:6379");
126```
127
128## Network Groups
129
130Network groups allow multiple apps to share services:
131
132```toml
133[network_groups]
134"backend" = ["app.mydomain.com", "api.mydomain.com"]
135
136[attachments]
137"backend" = ["shared-postgres:latest", "shared-redis:latest"]
138```
139
140Both `app.mydomain.com` and `api.mydomain.com` can access the shared services.
141
142## Volumes
143
144Gordon automatically creates persistent storage from Dockerfile `VOLUME` directives:
145
146```dockerfile
147FROM postgres:18
148VOLUME ["/var/lib/postgresql/data"]
149```
150
151Volume behavior:
152
153- **auto_create**: Volumes are created automatically (default: true)
154- **prefix**: Volume names are prefixed with `gordon-` (configurable)
155- **preserve**: Volumes persist across container updates (default: true)
156
157## Environment Variables
158
159Gordon loads environment variables from files based on the domain:
160
161```
162~/.gordon/env/
163├── app_mydomain_com.env
164├── api_mydomain_com.env
165└── admin_mydomain_com.env
166```
167
168Domain dots become underscores: `app.mydomain.com` → `app_mydomain_com.env`
169
170Variables are merged in order:
171
1721. Dockerfile `ENV` directives (lowest priority)
1732. `.env` file values (highest priority)
174
175### Secret Providers
176
177Environment files support secret provider syntax:
178
179```bash
180# From Unix password manager (pass)
181DATABASE_PASSWORD=${pass:myapp/db-password}
182
183# From SOPS encrypted files
184API_SECRET=${sops:secrets.yaml:api.secret}
185```
186
187## Configuration Hot-Reload
188
189Gordon watches its config file and reloads automatically:
190
1911. Edit `~/.config/gordon/gordon.toml`
1922. Save the file
1933. Gordon reloads hot-reloaded settings such as attachments, network groups, and routes
1944. Containers and proxy config sync to match the new configuration
195
196Route file edits now reload automatically again.
197
198You can also trigger a reload:
199
200```bash
201gordon reload
202```
203
204`gordon reload` sends `SIGUSR1` to the running Gordon process.
205
206## Event System
207
208Gordon uses an internal event system for coordination:
209
210| Event | Trigger | Action |
211|-------|---------|--------|
212| `image.pushed` | Image pushed to registry | Deploy container |
213| `config.reload` | Config file changed or `gordon reload` sends `SIGUSR1` | Reload config, sync containers, and refresh proxy state |
214| `manual.deploy` | `gordon deploy <domain>` command | Deploy specific route |
215| `container.deployed` | Container started | Update proxy cache |
216
217## Backups and Recovery
218
219Gordon can run logical PostgreSQL backups for attachment containers.
220
221Current design:
222
2231. Detect PostgreSQL attachments attached to a route
2242. Execute `pg_dump -Fc` through Gordon runtime operations
2253. Store backup artifacts on local filesystem storage
2264. Expose backup actions through admin API and CLI
227
228This is intentionally scoped for operational safety and predictable behavior.
229Future extensions can add physical backups, PITR, and remote object storage adapters.
230For 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).
231
232## Container Labels
233
234Gordon uses labels to track managed containers:
235
236| Label | Purpose |
237|-------|---------|
238| `gordon.managed=true` | Identifies Gordon-managed containers |
239| `gordon.domain` | Domain this container serves |
240| `gordon.image` | Image name and tag |
241| `gordon.route` | Route this container handles |
242| `gordon.attachment=true` | Container is an attachment service |
243| `gordon.attached-to` | Which route this attachment serves |
244
245## Proxy Port Selection
246
247When a container exposes multiple ports, Gordon needs to know which one serves HTTP:
248
249```dockerfile
250FROM gitea/gitea:latest
251LABEL gordon.proxy.port=3000  # Route HTTP to port 3000
252EXPOSE 22   # SSH
253EXPOSE 3000 # HTTP
254```
255
256Without the label, Gordon uses the first exposed port.
257
258## Related
259
260- [Configuration Reference](./config/index.md)
261- [Docker Labels Reference](./reference/docker-labels.md)
262- [Environment Variables](./config/env.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