# 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 ```bash gordon images push --build --remote https://gordon.example.com ``` Then activate (from CI with the Gordon binary, or from your machine): ```bash 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 ```bash # 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) ```bash 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. ```bash # 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: ```bash docker tag myapp gordon.example.com/myapp:latest docker push gordon.example.com/myapp:latest ``` Reference it from the app manifest: ```toml [services.web] image = "gordon.example.com/myapp:latest" ``` ### Semantic Versioning Pin services to specific versions and apply a new manifest to roll forward: ```bash 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: ```bash 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. ## Related - [GitHub Actions](./github-actions.md) - [GitLab CI](./gitlab-ci.md) - [Generic CI](./generic-ci.md) - [Apps CLI](../cli/apps.md) - [Authentication](../config/auth.md)