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.
# 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.
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)