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.