v3.1.0 d676137

index.md RawHistoryFolder

Deployment Overview

Gordon deploys apps in two explicit steps: push the image to its built-in registry, then apply the app manifest and deploy. Push transfers OCI content only — it never deploys, creates routes, or modifies manifests.

gordon images push --build --remote builds, pushes, and stores the image from CI/CD pipelines. Activation is a separate explicit step with the Gordon CLI.

  • Single secret (GORDON_TOKEN): auto-exchanges for a short-lived registry token
  • Auto-detects version from CI environment ($GITHUB_REF, $CI_COMMIT_TAG, $BUILD_SOURCEBRANCH, or git describe)
  • Chunked uploads (50MB chunks) — works behind Cloudflare and restrictive proxies
gordon images push --build --remote https://gordon.example.com

Then activate (from CI with the Gordon binary, or from your machine):

gordon apps apply --file blog.toml --remote https://gordon.example.com
gordon apps deploy blog --remote https://gordon.example.com

All Deployment Methods

Method Best For Secrets Needed Registry Access Deploy Control
gordon images push + apps deploy (Recommended) CI/CD pipelines 1 (GORDON_TOKEN) Via gordon domain (HTTPS) Explicit (CLI-triggered)
docker push + apps deploy Simple setups, existing Docker workflows 2 (username + token) Via gordon domain (HTTPS) Explicit (CLI-triggered)

The Gordon CLI handles authentication, image building, and registry upload in a single step. Deploy stays explicit.

  • Single token handles everything: admin API access + registry auth via automatic token exchange
  • Version tag auto-detected from $GITHUB_REF, $CI_COMMIT_TAG, $BUILD_SOURCEBRANCH, or git describe
  • Requires the Gordon binary on the CI runner
# Build and push (OCI transfer only)
gordon images push --build --remote https://gordon.example.com

# Apply the manifest that references the pushed tag, then deploy
gordon apps apply --file blog.toml --remote https://gordon.example.com
gordon apps deploy blog --remote https://gordon.example.com

Method 2: docker push + apps deploy

Standard Docker workflow — no Gordon binary required on the runner for the push itself.

  • Use docker login, docker build, and docker push as usual
  • Pushing only stores the image; deploy explicitly with the Gordon CLI afterwards
  • Registry endpoint is gordon.example.com (not a separate registry host)
echo "$GORDON_TOKEN" | docker login -u ci-bot --password-stdin gordon.example.com
docker build -t gordon.example.com/myapp:v1.2.0 .
docker push gordon.example.com/myapp:v1.2.0
# Then: gordon apps apply --file blog.toml + gordon apps deploy blog

Registry Access

Clients use the main Gordon domain over HTTPS, normally on port 443. Gordon binds server.registry_port to 127.0.0.1; the public edge proxies authenticated registry requests to that loopback listener. Do not publish or forward the loopback registry port. With auth.enabled=false, registry access is loopback-only and the public edge refuses registry-domain requests.

Network Topologies

Setup Configuration Use Case
Public (default) auth.enabled = true Hosted CI runners (GitHub Actions, GitLab CI)
Tailscale only registry_allowed_ips = ["100.64.0.0/10"] Self-hosted runners in Tailnet
Localhost only auth.enabled = false Local development, single-machine deploys

Token Setup

See the examples below for the right scopes for each workflow.

# Push only — registry scopes
gordon auth token generate \
  --subject ci-bot \
  --scopes "push,pull" \
  --expiry 90d

# Push + apply/deploy — adds app mutation scopes
gordon auth token generate \
  --subject ci-bot \
  --scopes "push,pull,admin:apps:read,admin:apps:write" \
  --expiry 90d

# Repository-scoped registry token for push only
gordon auth token generate \
  --subject ci-bot \
  --repo myapp \
  --scopes "push,pull" \
  --expiry 90d

Set the generated token as GORDON_TOKEN in your CI environment. --repo limits registry push/pull to that repository; it does not constrain admin:* scopes. Use a separate least-privilege token for app administration when repository and deployment duties must be isolated.

Version Strategies

Latest Tag

Always deploy the most recent build:

docker tag myapp gordon.example.com/myapp:latest
docker push gordon.example.com/myapp:latest

Reference it from the app manifest:

[services.web]
image = "gordon.example.com/myapp:latest"

Semantic Versioning

Pin services to specific versions and apply a new manifest to roll forward:

docker tag myapp gordon.example.com/myapp:v2.1.0
docker push gordon.example.com/myapp:v2.1.0

To deploy a new version, update the tag in the app file, apply, and deploy.

Git SHA Tags

Tag with commit hash for full traceability:

VERSION=$(git rev-parse --short HEAD)
docker tag myapp gordon.example.com/myapp:$VERSION
docker push gordon.example.com/myapp:$VERSION

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, and Gordon never runs two Gordon-managed generations of the same service at the same time.

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

Preflight (image resolution and pull, secrets, volumes, networks, bind policy, reservations) completes before anything is disrupted, so a preflight failure leaves the running service 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.

If a deploy is interrupted between creating the replacement container and publishing it, that candidate is recorded durably and Gordon removes it before creating or rebuilding 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. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default.

source
1# Deployment Overview
2
3Gordon deploys apps in two explicit steps: push the image to its built-in registry, then apply the app manifest and deploy. Push transfers OCI content only — it never deploys, creates routes, or modifies manifests.
4
5## Recommended: gordon images push + apps deploy
6
7`gordon images push --build --remote` builds, pushes, and stores the image from CI/CD pipelines. Activation is a separate explicit step with the Gordon CLI.
8
9- Single secret (`GORDON_TOKEN`): auto-exchanges for a short-lived registry token
10- Auto-detects version from CI environment (`$GITHUB_REF`, `$CI_COMMIT_TAG`, `$BUILD_SOURCEBRANCH`, or `git describe`)
11- Chunked uploads (50MB chunks) — works behind Cloudflare and restrictive proxies
12
13```bash
14gordon images push --build --remote https://gordon.example.com
15```
16
17Then activate (from CI with the Gordon binary, or from your machine):
18
19```bash
20gordon apps apply --file blog.toml --remote https://gordon.example.com
21gordon apps deploy blog --remote https://gordon.example.com
22```
23
24## All Deployment Methods
25
26| Method | Best For | Secrets Needed | Registry Access | Deploy Control |
27|--------|----------|----------------|-----------------|----------------|
28| `gordon images push` + `apps deploy` (Recommended) | CI/CD pipelines | 1 (`GORDON_TOKEN`) | Via gordon domain (HTTPS) | Explicit (CLI-triggered) |
29| `docker push` + `apps deploy` | Simple setups, existing Docker workflows | 2 (`username` + `token`) | Via gordon domain (HTTPS) | Explicit (CLI-triggered) |
30
31### Method 1: gordon images push + apps deploy (Recommended)
32
33The Gordon CLI handles authentication, image building, and registry upload in a single step. Deploy stays explicit.
34
35- Single token handles everything: admin API access + registry auth via automatic token exchange
36- Version tag auto-detected from `$GITHUB_REF`, `$CI_COMMIT_TAG`, `$BUILD_SOURCEBRANCH`, or `git describe`
37- Requires the Gordon binary on the CI runner
38
39```bash
40# Build and push (OCI transfer only)
41gordon images push --build --remote https://gordon.example.com
42
43# Apply the manifest that references the pushed tag, then deploy
44gordon apps apply --file blog.toml --remote https://gordon.example.com
45gordon apps deploy blog --remote https://gordon.example.com
46```
47
48### Method 2: docker push + apps deploy
49
50Standard Docker workflow — no Gordon binary required on the runner for the push itself.
51
52- Use `docker login`, `docker build`, and `docker push` as usual
53- Pushing only stores the image; deploy explicitly with the Gordon CLI afterwards
54- Registry endpoint is `gordon.example.com` (not a separate registry host)
55
56```bash
57echo "$GORDON_TOKEN" | docker login -u ci-bot --password-stdin gordon.example.com
58docker build -t gordon.example.com/myapp:v1.2.0 .
59docker push gordon.example.com/myapp:v1.2.0
60# Then: gordon apps apply --file blog.toml + gordon apps deploy blog
61```
62
63## Registry Access
64
65Clients use the main Gordon domain over HTTPS, normally on port 443. Gordon binds `server.registry_port` to `127.0.0.1`; the public edge proxies authenticated registry requests to that loopback listener. Do not publish or forward the loopback registry port. With `auth.enabled=false`, registry access is loopback-only and the public edge refuses registry-domain requests.
66
67### Network Topologies
68
69| Setup | Configuration | Use Case |
70|-------|--------------|----------|
71| Public (default) | `auth.enabled = true` | Hosted CI runners (GitHub Actions, GitLab CI) |
72| Tailscale only | `registry_allowed_ips = ["100.64.0.0/10"]` | Self-hosted runners in Tailnet |
73| Localhost only | `auth.enabled = false` | Local development, single-machine deploys |
74
75## Token Setup
76
77See the examples below for the right scopes for each workflow.
78
79```bash
80# Push only — registry scopes
81gordon auth token generate \
82  --subject ci-bot \
83  --scopes "push,pull" \
84  --expiry 90d
85
86# Push + apply/deploy — adds app mutation scopes
87gordon auth token generate \
88  --subject ci-bot \
89  --scopes "push,pull,admin:apps:read,admin:apps:write" \
90  --expiry 90d
91
92# Repository-scoped registry token for push only
93gordon auth token generate \
94  --subject ci-bot \
95  --repo myapp \
96  --scopes "push,pull" \
97  --expiry 90d
98```
99
100Set the generated token as `GORDON_TOKEN` in your CI environment. `--repo` limits registry push/pull to that repository; it does not constrain `admin:*` scopes. Use a separate least-privilege token for app administration when repository and deployment duties must be isolated.
101
102## Version Strategies
103
104### Latest Tag
105
106Always deploy the most recent build:
107
108```bash
109docker tag myapp gordon.example.com/myapp:latest
110docker push gordon.example.com/myapp:latest
111```
112
113Reference it from the app manifest:
114
115```toml
116[services.web]
117image = "gordon.example.com/myapp:latest"
118```
119
120### Semantic Versioning
121
122Pin services to specific versions and apply a new manifest to roll forward:
123
124```bash
125docker tag myapp gordon.example.com/myapp:v2.1.0
126docker push gordon.example.com/myapp:v2.1.0
127```
128
129To deploy a new version, update the tag in the app file, apply, and deploy.
130
131### Git SHA Tags
132
133Tag with commit hash for full traceability:
134
135```bash
136VERSION=$(git rev-parse --short HEAD)
137docker tag myapp gordon.example.com/myapp:$VERSION
138docker push gordon.example.com/myapp:$VERSION
139```
140
141## Updates
142
143Gordon 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, and Gordon never runs two Gordon-managed generations of the same service at the same time.
144
145```
146Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic
147```
148
149Preflight (image resolution and pull, secrets, volumes, networks, bind policy, reservations) completes before anything is disrupted, so a preflight failure leaves the running service 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.
150
151If a deploy is interrupted between creating the replacement container and publishing it, that candidate is recorded durably and Gordon removes it before creating or rebuilding 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. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default.
152
153## Related
154
155- [GitHub Actions](./github-actions.md)
156- [GitLab CI](./gitlab-ci.md)
157- [Generic CI](./generic-ci.md)
158- [Apps CLI](../cli/apps.md)
159- [Authentication](../config/auth.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