Upgrading Gordon
This guide covers breaking changes and migration steps between major versions.
v3.0.0: Declarative Apps (breaking)
Gordon v3 replaces route-container management with declarative apps. One standalone TOML file defines one app; gordon apps apply persists desired state and gordon apps deploy activates it. Push transfers OCI content only and never deploys.
Follow Migrate to Gordon v3 for the complete cutover procedure, including explicit migration from domain-scoped secrets to app- and service-scoped secrets.
Removed
- App-workload keys in
gordon.toml:[routes],[attachments],[network_groups],[service_routes],[auto_route](+_allowed_domains), and[previews]. Gordon fails boot/reload closed with aconfig-retireddiagnostic naming the fix when any of them is present — never a silent migration. - Installation-level
[[services]](standalone L4 workloads) and[[network_services]](L4 traffic plane) stay valid. Only their old app-workload semantics were removed; declare application workloads in standalone files instead. See Standalone Services. - CLI:
pin,preview,attachments,bootstrap,autoroute allow,routes add/remove/purge, push deploy/route inference, implicit deploys on push/reload, label/env-file inference. Removed HTTP mutation endpoints answer410 Gone. - Domain secrets:
gordon secrets,/admin/secrets(now410 Gone), theadmin:secrets:*scopes, the[env]config section, and startup.envimport into pass. Usegordon apps secrets. Existinggordon/env/...pass entries are left untouched. - CLI layout:
gordon pushis nowgordon images push.status,logs,reload,config,tls status,traffic status, andnetworks listmoved undergordon daemon(gordon daemon status,gordon daemon tls,gordon daemon traffic,gordon daemon networks, …). There are no aliases. - Scopes
admin:routes:*andadmin:secrets:*are replaced byadmin:apps:read(list, show, diff, status) andadmin:apps:write(apply, deploy, lifecycle, secrets). Regenerate CI tokens, e.g.--scopes "push,pull,admin:apps:read,admin:apps:write". - No historical rollback command: roll back by applying a manifest that references the previous tag and deploying again.
Migrating a v3 alpha app manifest
Early v3 alpha manifests used the array-of-tables form [[service]] with a name field and [service.*] children. That shape is rejected with a keyed-schema diagnostic, not converted. Rewrite each service as a [services.<name>] table.
Before (alpha, rejected):
name = "blog"
[[service]]
name = "web"
image = "registry.example.com/blog/web:1.2.3"
[[service.http]]
host = "blog.example.com"
port = 8080
After (current keyed schema):
name = "blog"
[services.web]
image = "registry.example.com/blog/web:1.2.3"
[[services.web.http]]
host = "blog.example.com"
port = 8080
Names containing dots must be quoted: [services."web.api"] and [[services."web.api".http]]. See App Manifest.
Manual migration
- Back up databases/volumes and pass entries with the existing procedures. Record original ownership and image versions. No update hook deletes volumes: unknown resources are preserved, never adopted.
- Delete the removed keys, including
[env], fromgordon.toml(installation settings only: entrypoints, TLS, limits, external routes, images policy, backups destinations stay). - Write one
<app>.tomlper app (see App Manifest): services,[[services.<name>.http]]hosts,[services.<name>.secrets]names, volumes,[[network.shared]], backup declarations. - Migrate secret values explicitly with
gordon apps secrets setafter applying each manifest. Gordon does not copygordon/env/<domain>/...entries intogordon/apps/<uuid>/<service>/...; follow the v3 secrets migration procedure. gordon apps apply --file <app>.toml, thengordon apps deploy <app>.- Staging is an ordinary app in another file. A binary downgrade against the new app-state format is unsupported: restore the old installation/config/state and backups through an operator-approved procedure.
Route-Domain Validation
Route keys must be plain hostnames. Use inline tables like "app.example.com" = { image = "myapp:latest" }. Gordon still reads legacy http://... route entries for backward compatibility and rewrites them on the next save. Update [routes], CLI commands, and automation that reference the old values.
v2.31.0: Unified smart TCP entrypoints
Breaking: server.port / server.tls_port no longer define public listeners
Public application traffic is configured entrypoint-first. Migrate old HTTP/HTTPS listener settings to a smart TCP edge entrypoint.
Before:
[server]
port = 8088
tls_port = 8443
registry_port = 5000
gordon_domain = "gordon.example.com"
After:
[server]
registry_port = 5000
gordon_domain = "gordon.example.com"
[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"
edge is the conventional route default, but it has no built-in port. address = ":443", address = ":9000", firewall forwarding, and container mappings such as -p 443:9000 are deployment choices, not protocol modes. The smart_tcp protocol accepts TCP, sniffs HTTP/h2c/TLS, routes TLS passthrough by SNI when configured, and otherwise uses normal Gordon HTTPS fallback.
If you run rootless and cannot bind 443 directly, bind a high port and map external ports to it:
[entrypoints.edge]
address = ":9000"
protocol = "smart_tcp"
sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=9000
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
sudo firewall-cmd --reload
Keep server.registry_port for Docker/Podman push and pull traffic; it is separate from public app entrypoints.
ACME migration notes
- DNS-01 (
cloudflare-dns-01) does not require a special external port 80 edge. - HTTP-01 requires an HTTP-capable
smart_tcpentrypoint reachable on external port 80 for each hostname being validated. - TLS-ALPN-01 is unsupported.
- Normal HTTPS fallback certificate priority is static certificates, then public ACME certificates, then Gordon's internal CA.
Required for Cloudflare/Proxy Setups: proxy_allowed_ips
The internal CA's HTTP onboarding gate rejects non-localhost HTTP requests by default. If Gordon sits behind Cloudflare or another reverse proxy, add the proxy's edge IPs to proxy_allowed_ips:
[server]
proxy_allowed_ips = [
"173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
"103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
"190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
"198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
"104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
]
Without this, all proxied HTTP traffic returns 403 Forbidden. Remove TLS-capable entrypoints to disable the internal CA and skip this requirement.
Breaking: server.gordon_domain Replaces server.registry_domain
Gordon now uses server.gordon_domain as the public registry and admin host. Migrate older configs that still set only server.registry_domain before restarting:
Before:
[server]
registry_domain = "gordon.example.com"
After:
[server]
gordon_domain = "gordon.example.com"
If you do not migrate, gordon daemon status --remote ... and gordon apps list --remote ... can fail with /auth/token 404, and reg-domain/v2/ or /admin/status can return 404.
Staged Registry Host Rename
If you cannot rename the Gordon registry host in one step, keep the new host in server.gordon_domain and list the old Gordon registry hosts in server.legacy_registry_domains during the cutover:
[server]
gordon_domain = "gordon.example.com"
legacy_registry_domains = [
"registry.example.com",
"registry.example.com:5000",
]
Recommended rollout:
- Set
gordon_domainto the new host. - Add every old Gordon registry host that clients still use to
legacy_registry_domains. - Restart Gordon.
- Move Docker/Podman logins, pushes, pulls, and image references to the new host.
- Remove
legacy_registry_domainsafter every client has moved.
During the transition, Gordon treats both the current and legacy hosts as its own registry for image matching and internal pulls, then saves canonical refs back to gordon_domain. Remote CLI and admin API traffic should use the new gordon_domain.
Breaking: gordon rollback Renamed to gordon pin
If you have scripts or runbooks using gordon rollback, switch them to gordon pin:
gordon pin app.example.com
gordon pin app.example.com --tag v2.30.1
gordon pin list app.example.com
Breaking-ish: routes list Is Inventory-Only
gordon routes list now shows domain/image inventory only. Detailed runtime state, HTTP probe status, and attachment status moved to gordon routes status.
If you have automation parsing status-like fields from routes list, migrate it to routes status --json.
New: Public ACME TLS
Gordon can now obtain public certificates with [tls.acme] using either http-01 or cloudflare-dns-01.
- Normal HTTPS fallback must be available on a TLS-capable entrypoint
http-01needs an HTTP-capable smart TCP entrypoint reachable on external port 80 for each hostname being validatedcloudflare-dns-01needs a Cloudflare token with zone-read and DNS-edit access for every zone used by HTTPS routes and does not require a special port-80 edgeobtain_batch_sizelimits new ACME orders per reconcile pass to reduce rate-limit spikes
New: DNS Resolver Settings for ACME DNS-01
[dns] controls the recursive resolvers Gordon uses to verify public DNS propagation for ACME DNS-01.
Add this if you need non-default resolvers, split-horizon-aware testing, or a slower propagation window:
[dns]
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
propagation_timeout = "5m"
polling_interval = "5s"
Runtime Change: Managed Containers Use Restart Policy always
Newly created managed route containers now use the runtime restart policy always. Existing containers pick this up on redeploy/recreate.
This improves crash recovery and keeps Docker/Podman behavior aligned with Gordon's startup reconciliation.
New: Saved Remote Inference for Targeted Commands
When you do not pass --remote, Gordon can now infer a saved remote for some target-based commands when exactly one configured remote matches the route, image, attachment target, or repository.
If multiple remotes match or probing fails, Gordon now stops and asks you to pass --remote explicitly instead of guessing.
New: Dedicated Access Log
You can now enable logging.access_log.* for a dedicated HTTP access log separate from the main process log. This is useful for request auditing, GoAccess, CrowdSec, or fail2ban-style tooling.
v2.16.0 to v2.30.0
Breaking: Password Authentication Removed
Gordon v2.30.0 removes password-based authentication entirely. Only token-based authentication is supported.
What changed:
auth.type = "password"is no longer acceptedauth.passwordandauth.password_hashconfig fields are removed- The
gordon auth password hashCLI command is removed - The
gordon auth logincommand now requires--token(no more interactive password prompt) - The
/auth/passwordendpoint now returns410 Gone - Long-lived tokens are no longer accepted on admin/registry endpoints — they must be exchanged for ephemeral tokens via
/auth/token
New features:
auth.access_token_ttlconfigures the lifetime of ephemeral access tokens issued by/auth/token(default:"15m")gordon auth show-tokenprints the stored token for a remotegordon auth logoutremoves the stored token locally- Automatic token exchange: the CLI transparently exchanges long-lived tokens for ephemeral ones before API calls
- Admin scopes (
admin:*:*,admin:apps:read,admin:apps:write, etc.) allow fine-grained access control for remote CLI operations
Migration steps:
Before upgrading, generate a token on your current Gordon instance:
gordon auth token generate --subject deploy --scopes "push,pull" --expiry 0Save this token securely. You will need it after upgrading. Admin scopes (
admin:*:*) are only available after upgrading to v2.30.0 — regenerate your token with admin scopes after the upgrade if needed.Update remotes to use the generated token:
gordon auth login --token <token> # or gordon remotes set-token prod <token>Update your config to remove password fields:
Before (v2.16.0):
[auth] enabled = true secrets_backend = "pass" username = "deploy" password_hash = "gordon/auth/password_hash" token_secret = "gordon/auth/token_secret"After (v2.30.0):
[auth] enabled = true secrets_backend = "pass" token_secret = "gordon/auth/token_secret" access_token_ttl = "15m"Update CI/CD pipelines to use
GORDON_TOKENenvironment variable for authentication. See the deployment guides.Upgrade the binary and restart:
curl -fsSL https://bnema.dev/gordon/install | bash systemctl --user restart gordonVerify the server starts without errors:
journalctl --user -u gordon -f
New: Configurable Access Token TTL
Ephemeral access tokens issued by /auth/token now have a configurable lifetime via auth.access_token_ttl (default "15m", maximum "1h").
Tokens with a lifetime at or below MaxAccessTokenLifetime (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they cannot be individually revoked. They become invalid only when they naturally expire. Shortening auth.access_token_ttl only reduces the exposure window; it does not enable per-token revocation. If you need explicit revocation, use a stored long-lived token instead of /auth/token.
[auth]
access_token_ttl = "30m"
New: Admin Scopes
Tokens can now include fine-grained admin scopes for remote CLI operations:
# Full admin access
gordon auth token generate --subject admin --scopes "push,pull,admin:*:*" --expiry 0
# Read-only monitoring
gordon auth token generate --subject monitor --scopes "admin:status:read" --expiry 30d
# CI deploy with app read + write
gordon auth token generate --subject ci --scopes "push,pull,admin:apps:read,admin:apps:write" --expiry 0
See Token Scopes for the full list.
v2.6.0 to v2.7.0
Breaking: Token Secret Required
Gordon v2.7.0 requires a token_secret for JWT authentication. The server will not start without it configured.
Error you'll see:
Error: token_secret is required for JWT token generation; set GORDON_AUTH_TOKEN_SECRET environment variable or configure auth.token_secret
Choose one of these migration options:
Option A: Environment Variable (simplest)
# Generate a random secret
export GORDON_AUTH_TOKEN_SECRET="$(openssl rand -base64 32)"
# Add to your shell profile or systemd service
echo 'export GORDON_AUTH_TOKEN_SECRET="your-secret"' >> ~/.bashrc
For systemd services:
mkdir -p ~/.config/systemd/user/gordon.service.d
cat > ~/.config/systemd/user/gordon.service.d/token.conf << EOF
[Service]
Environment="GORDON_AUTH_TOKEN_SECRET=$(openssl rand -base64 32)"
EOF
systemctl --user daemon-reload
systemctl --user restart gordon
Option B: Config File with Pass (recommended for production)
# Generate and store secret in pass
openssl rand -base64 32 | pass insert -e gordon/auth/token_secret
# Update your gordon.toml
[auth]
enabled = true
secrets_backend = "pass"
token_secret = "gordon/auth/token_secret"
Option C: Config File with Unsafe Backend (development only)
# Generate secret file
mkdir -p ~/.gordon/secrets
openssl rand -base64 32 > ~/.gordon/secrets/token_secret
chmod 600 ~/.gordon/secrets/token_secret
[auth]
enabled = true
secrets_backend = "unsafe"
token_secret = "token_secret"
Breaking: Unsafe Token Store Warning
Using secrets_backend = "unsafe" now logs a warning on startup:
WRN using unsafe secrets backend - secrets are stored in plain text
This is intentional to encourage secure secret storage in production. The warning can be ignored for development.
New Features
- Attachment secrets discovery:
gordon secrets list <domain>now shows secrets for attachment containers - Auth login command:
gordon auth login --remote <name>for token authentication - Rate limiting: Configurable rate limits under
[api.rate_limit]
Security Improvements
v2.7.0 includes a comprehensive security audit with:
- JWT tokens now include "not before" (nbf) claim
- SSRF protection for external routes
- Security headers middleware
- Rate limiting on registry and token endpoints
- Command injection prevention in pass provider
- Path traversal prevention in secrets store
v2.5.0 to v2.6.0
Breaking: Config Restructure
The [secrets] and [registry_auth] sections were merged into [auth].
Before (v2.5.0):
[secrets]
backend = "pass"
[registry_auth]
enabled = true
type = "password"
password_hash = "gordon/registry/password_hash"
After (v2.6.0+):
[auth]
enabled = true
secrets_backend = "pass"
password_hash = "gordon/auth/password_hash"
Breaking: Auth Enabled by Default
Gordon keeps auth enabled by default. If you set auth.enabled = false, Gordon runs in local-only mode (/admin/* disabled, /v2/* loopback-only).
Breaking: Secret Paths Changed
If using pass or sops, update your secret paths:
gordon/registry/*→gordon/auth/*
General Upgrade Process
- Read the changelog for your target version
- Backup your config before upgrading
- Test in staging if possible
- Upgrade the binary:
curl -fsSL https://bnema.dev/gordon/install | bash - Restart Gordon:
systemctl --user restart gordon - Check logs for any errors:
gordon daemon logs