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.
Recommended: gordon images push + apps deploy
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) |
Method 1: gordon images push + apps deploy (Recommended)
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.
# docker push only — registry scopes
gordon auth token generate \
--subject ci-bot \
--scopes "push,pull" \
--expiry 90d
# gordon images push only — the CLI also needs one admin scope
gordon auth token generate \
--subject ci-bot \
--scopes "push,pull,admin:apps:read" \
--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 docker 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. gordon images push calls the admin API before it exchanges the token for registry access, so its token needs at least one admin:* scope; admin:apps:read is the smallest. --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.
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 stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; a service that owns a volume, bind, or device is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of such a service at the same time. A UDP service is also replaced sequentially: UDP sessions stay pinned to the backend they started on, so they could not follow the switch.
Stateless service (no volume, bind, device, or UDP interface):
Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old
Service with a volume, bind, device, or UDP interface:
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.
For a stateless service the old container serves until the new one is ready and routed, then receives SIGTERM and is killed after its stop_grace (default 30s). During the overlap both containers share the service's network alias, so other services of the app may reach either one. Database migrations must stay backward compatible with the previous version of the service. A new container that fails readiness is removed and the old one is not touched. An old container that cannot be removed is reported as a cleanup warning without failing the deploy; it stays durably marked for retirement, and the periodic recovery pass retries it with its own stop_grace once that pass has verified the new container, even after later deploys. A retirement is only complete once the old container's loopback port claims are released; until then it is retried. gordon apps stop and gordon apps remove also stop such an old container, and fail, keeping the mark, if they cannot.
For a sequential replacement, a failure after the old container was removed 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; a stateless service's old container keeps serving. If it is interrupted after the switch but before the old container was stopped, the periodic recovery pass verifies and routes the published container and then stops the old one. 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. 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# docker push only — registry scopes
81gordon auth token generate \
82 --subject ci-bot \
83 --scopes "push,pull" \
84 --expiry 90d
85
86# gordon images push only — the CLI also needs one admin scope
87gordon auth token generate \
88 --subject ci-bot \
89 --scopes "push,pull,admin:apps:read" \
90 --expiry 90d
91
92# Push + apply/deploy — adds app mutation scopes
93gordon auth token generate \
94 --subject ci-bot \
95 --scopes "push,pull,admin:apps:read,admin:apps:write" \
96 --expiry 90d
97
98# Repository-scoped registry token for docker push only
99gordon auth token generate \
100 --subject ci-bot \
101 --repo myapp \
102 --scopes "push,pull" \
103 --expiry 90d
104```
105
106Set the generated token as `GORDON_TOKEN` in your CI environment. `gordon images push` calls the admin API before it exchanges the token for registry access, so its token needs at least one `admin:*` scope; `admin:apps:read` is the smallest. `--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.
107
108## Version Strategies
109
110### Latest Tag
111
112Always deploy the most recent build:
113
114```bash
115docker tag myapp gordon.example.com/myapp:latest
116docker push gordon.example.com/myapp:latest
117```
118
119Reference it from the app manifest:
120
121```toml
122[services.web]
123image = "gordon.example.com/myapp:latest"
124```
125
126### Semantic Versioning
127
128Pin services to specific versions and apply a new manifest to roll forward:
129
130```bash
131docker tag myapp gordon.example.com/myapp:v2.1.0
132docker push gordon.example.com/myapp:v2.1.0
133```
134
135To deploy a new version, update the tag in the app file, apply, and deploy.
136
137### Git SHA Tags
138
139Tag with commit hash for full traceability:
140
141```bash
142VERSION=$(git rev-parse --short HEAD)
143docker tag myapp gordon.example.com/myapp:$VERSION
144docker push gordon.example.com/myapp:$VERSION
145```
146
147## Updates
148
149Gordon 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; a service that owns a volume, bind, or device is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of such a service at the same time. A UDP service is also replaced sequentially: UDP sessions stay pinned to the backend they started on, so they could not follow the switch.
150
151Stateless service (no volume, bind, device, or UDP interface):
152
153```
154Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old
155```
156
157Service with a volume, bind, device, or UDP interface:
158
159```
160Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic
161```
162
163Preflight (image resolution and pull, secrets, volumes, networks, bind policy, reservations) completes before anything is disrupted, so a preflight failure leaves the running service untouched.
164
165For a stateless service the old container serves until the new one is ready and routed, then receives SIGTERM and is killed after its `stop_grace` (default 30s). During the overlap both containers share the service's network alias, so other services of the app may reach either one. Database migrations must stay backward compatible with the previous version of the service. A new container that fails readiness is removed and the old one is not touched. An old container that cannot be removed is reported as a cleanup warning without failing the deploy; it stays durably marked for retirement, and the periodic recovery pass retries it with its own `stop_grace` once that pass has verified the new container, even after later deploys. A retirement is only complete once the old container's loopback port claims are released; until then it is retried. `gordon apps stop` and `gordon apps remove` also stop such an old container, and fail, keeping the mark, if they cannot.
166
167For a sequential replacement, a failure after the old container was removed 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.
168
169If 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; a stateless service's old container keeps serving. If it is interrupted after the switch but before the old container was stopped, the periodic recovery pass verifies and routes the published container and then stops the old one. 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. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default.
170
171## Related
172
173- [GitHub Actions](./github-actions.md)
174- [GitLab CI](./gitlab-ci.md)
175- [Generic CI](./generic-ci.md)
176- [Apps CLI](../cli/apps.md)
177- [Authentication](../config/auth.md)