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, orgit 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, orgit 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, anddocker pushas 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.