main bac21c6

server.md RawHistoryFolder

Server Configuration

Core server settings for Gordon.

Configuration

[server]
registry_port = 5000                     # Container registry port
gordon_domain = "gordon.mydomain.com"    # Gordon domain (required)
# tls_cert_file = ""                       # Optional: PEM certificate for static TLS
# tls_key_file = ""                        # Optional: PEM key for static TLS
# force_https_redirect = false             # Redirect cleartext HTTP requests to HTTPS
# legacy_registry_domains = ["registry.example.com:5000"]
# data_dir = "~/.gordon"                 # Data storage directory (default)
max_blob_chunk_size = "95MB"             # Max registry blob upload chunk
max_blob_size = "1GB"                    # Max cumulative registry blob/layer upload

[entrypoints.edge]
address = ":443"                         # Deployment-selected public TCP socket
protocol = "smart_tcp"

Options

Option Type Default Description
registry_port int 5000 Docker registry port for image push/pull
tls_cert_file string "" Path to PEM certificate file. When set with tls_key_file, matching SNI domains use this static cert before ACME or internal CA certs
tls_key_file string "" Path to PEM private key file. Must be set together with tls_cert_file
force_https_redirect bool false Redirect cleartext HTTP requests to HTTPS on HTTP-capable edge entrypoints
gordon_domain string required Domain for Gordon (registry + admin API)
registry_domain string - Deprecated migration key. Set gordon_domain instead.
legacy_registry_domains []string [] Additional Gordon registry hosts treated as aliases during staged migration. See Upgrading: Staged Registry Host Rename.
data_dir string ~/.gordon Directory for registry data, logs, and state
max_proxy_body_size string "512MB" Maximum request body size for proxied requests
max_blob_chunk_size string "95MB" Maximum request body size for a single registry blob upload chunk
max_blob_size string "1GB" Maximum cumulative size for one registry blob/layer upload
registry_allowed_ips []string [] IPs or CIDR ranges allowed to access the registry (empty = allow all)
proxy_allowed_ips []string [] IPs or CIDR ranges allowed to reach HTTP proxy paths (empty = allow all)
registry_listen_address string "" Bind address for registry (empty = all interfaces)

Public application traffic is configured under [entrypoints]. The standard edge listener is a smart_tcp socket:

[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"

entrypoints.edge.address is not an HTTP or HTTPS port. It is the TCP address Gordon sniffs for cleartext HTTP, h2c, TLS passthrough, normal HTTPS fallback, or explicit raw TCP fallback. Gordon does not provide a built-in default public port; choose :443, a high port such as :9000, or container/firewall mappings to match your deployment.

Registry HTTP Timeouts

The registry listeners (registry_port, plus the loopback listener used for the internal registry) apply fixed per-connection timeouts. Values are Go duration strings; malformed, zero, or negative values are rejected when the configuration loads.

[server.http]
read_header_timeout = "10s"
read_timeout = "5m"
write_timeout = "5m"
idle_timeout = "120s"
Option Env Default Description
server.http.read_header_timeout GORDON_SERVER_HTTP_READ_HEADER_TIMEOUT "10s" Maximum time to read request headers
server.http.read_timeout GORDON_SERVER_HTTP_READ_TIMEOUT "5m" Maximum time to read the whole request, body included
server.http.write_timeout GORDON_SERVER_HTTP_WRITE_TIMEOUT "5m" Maximum time to write the whole response
server.http.idle_timeout GORDON_SERVER_HTTP_IDLE_TIMEOUT "120s" Maximum keep-alive idle time between requests

read_timeout and write_timeout bound a whole registry request or response, so a layer transfer longer than these values is cut even while data flows. Raise them for very large layers over slow links. Traffic behind entrypoints (apps and the admin API) uses an inactivity timeout instead; see Traffic HTTP Timeouts. The owner-only local admin socket has no write timeout so log streaming stays open. Changes require a restart.

Internal CA and TLS

Gordon includes an internal certificate authority that issues on-demand TLS certificates for proxied domains served through HTTPS fallback on a TLS-capable entrypoint such as smart_tcp.

With TLS-capable edge traffic:

  • Gordon runs an internal CA (root + intermediate) stored in {data_dir}/pki/
  • Leaf certificates are issued on-demand per domain (SNI-based) and cached in memory
  • The intermediate CA auto-renews before expiry
  • An onboarding page at https://<gordon-host>/.well-known/gordon/ lets clients download the root CA certificate
  • The root CA is stable across restarts (generated once, persisted to disk)

Custom certificates

To use your own certificate (e.g. from Tailscale, Let's Encrypt, or a corporate CA) alongside ACME/internal CA fallback:

[server]
tls_cert_file = "/etc/gordon/tls/cert.pem"
tls_key_file = "/etc/gordon/tls/key.pem"

Certificate priority for normal HTTPS fallback is: static certificates first, then public ACME certificates, then Gordon's internal CA.

Public ACME certificates

Gordon can obtain public certificates through Let's Encrypt-compatible ACME when [tls.acme] is enabled. Normal HTTPS fallback must be available on a TLS-capable entrypoint.

[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"

[dns]
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
propagation_timeout = "5m"
polling_interval = "5s"

[tls.acme]
enabled = true
email = "admin@example.com"
challenge = "auto"       # auto, http-01, or cloudflare-dns-01
obtain_batch_size = 1    # maximum new certificate orders per reconcile run

Challenge behavior:

  • cloudflare-dns-01 uses a Cloudflare API token from pass, GORDON_CLOUDFLARE_API_TOKEN_FILE, or GORDON_CLOUDFLARE_API_TOKEN; it does not need a special public port 80 edge.
  • http-01 serves /.well-known/acme-challenge/<token> through Gordon's HTTP handler and requires an HTTP-capable smart_tcp entrypoint reachable on external port 80 for each hostname being validated.
  • auto selects Cloudflare DNS-01 when a token is available, otherwise falls back to HTTP-01.
  • tls-alpn-01 is not supported.

HTTP-01 requires public access to external port 80 for each hostname being validated. If you use Cloudflare in DNS-only/gray-cloud mode, your firewall/NAT must allow direct public traffic to the HTTP-capable smart TCP entrypoint. A firewall rule that only allows Cloudflare source IPs on port 80 is compatible with orange-cloud proxying, but it blocks gray-cloud HTTP-01 validation; use DNS-01 or temporarily open port 80 for direct validation.

Gordon automatically includes server.gordon_domain in public ACME coverage in addition to app HTTPS hosts. The management hostname therefore needs the same challenge reachability: public port 80 for HTTP-01, or zone-read and DNS-edit permissions for its zone with Cloudflare DNS-01.

Gordon limits new ACME certificate orders to obtain_batch_size per reconcile run (default 1) so enabling ACME on an existing multi-route server does not burst through every route and hit Let's Encrypt rate limits. The management hostname consumes a place in the same batch; later reloads, restarts, or other explicit reconcile runs continue issuing remaining certificates.

If the initial ACME reconcile fails (e.g. due to a transient network error or misconfiguration), Gordon logs the failure and keeps serving any existing certificates. The renewal loop retries reconcile work periodically, so missing certificates self-heal after the underlying issue is fixed without requiring a restart.

For Cloudflare Full/Strict, Cloudflare terminates browser TLS at the edge and connects to Gordon over HTTPS. A public ACME certificate served by Gordon is the preferred origin certificate because Cloudflare Strict can validate it without custom origin trust. Cloudflare Flexible mode (HTTPS at the edge, HTTP to Gordon) is not end-to-end HTTPS.

Static certificates have priority, then public ACME certificates, then Gordon's internal CA fallback. Gordon uses the go-acme/lego ACME client, including its DNS-provider support for Cloudflare DNS-01.

DNS-01 uses the Cloudflare API to create TXT records, then checks public DNS visibility through [dns].resolvers. These resolvers are recursive resolvers (for example Cloudflare DNS or Google DNS), not the authoritative DNS provider. A domain can be hosted at Cloudflare while Gordon verifies propagation through Google DNS.

The defaults avoid relying on host-local DNS. This matters on hosts using Tailscale MagicDNS, split-horizon corporate DNS, or Pi-hole, where /etc/resolv.conf may not reflect public DNS visibility as Let's Encrypt sees it.

Because Gordon's ACME challenge mode is global, cloudflare-dns-01 requires a Cloudflare token that can read zones and edit DNS records for every zone used by app HTTPS hosts. If that is not desirable, use http-01 until Gordon supports per-route or per-zone challenge policy.

Direct HTTP CA onboarding paths (/.well-known/gordon/ca)

When Gordon is serving TLS-capable edge traffic, direct cleartext HTTP clients (those not arriving through a trusted proxy) are restricted to CA onboarding paths only:

  • /.well-known/gordon/, /.well-known/gordon/ca, /.well-known/gordon/ca.crt, /.well-known/gordon/ca.mobileconfig — serve the onboarding page and CA certificate downloads
  • /.well-known/acme-challenge/* — serves ACME HTTP-01 challenges when public ACME HTTP-01 is enabled, otherwise returns 404 Not Found
  • All other paths return 403 Forbidden

This lets new clients discover and trust the internal CA over plain HTTP without exposing the full application. Trusted proxy traffic (e.g. from Cloudflare) continues through the normal HTTP proxy path unaffected.

On HTTPS, onboarding paths are served only on server.gordon_domain; app hosts are served without Gordon intercepting them. If gordon_domain is empty, HTTPS onboarding paths are not served at all to avoid intercepting app hosts.

HTTP to HTTPS redirect

When clients connect directly (no Cloudflare or reverse proxy in front), enable force_https_redirect to redirect cleartext HTTP traffic to HTTPS on the same smart TCP edge.

[server]
force_https_redirect = true

When proxy_allowed_ips is configured, non-proxy clients are redirected automatically even without this flag — trusted proxy IPs pass through to serve Cloudflare-proxied traffic.

Firewall and Container Port Mapping

Choose the bind address in [entrypoints.edge] and map external ports to it as needed. For example, a rootless service can bind a high port while the firewall exposes 443:

[entrypoints.edge]
address = ":9000"
protocol = "smart_tcp"
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
sudo firewall-cmd --reload

:443, :9000, and container -p 443:9000 mappings are deployment choices, not protocol modes.

Registry Port

The registry_port setting controls where Gordon accepts image pushes:

[server]
registry_port = 5000

Docker/Podman clients push to this port:

docker push registry.mydomain.com:5000/myapp:latest

If the registry is served over plain HTTP (no TLS), clients must be configured to allow insecure access. See Troubleshooting: insecure registry for Docker and Podman instructions.

Gordon Domain

The gordon_domain is required and must match your DNS configuration:

[server]
gordon_domain = "gordon.mydomain.com"

This domain is used for:

  • Docker login and image push/pull operations
  • Admin API access (/admin/* endpoints)
  • CLI remote targeting (gordon apps --remote https://gordon.mydomain.com)
  • Authentication endpoints (/auth/*)

Warning: If you are upgrading an older config, copy server.registry_domain to server.gordon_domain before restarting.

For a staged rename, set gordon_domain to the new public host and add any old Gordon registry hosts that clients still use to legacy_registry_domains (including host:port forms). Gordon treats those entries as registry aliases during image matching and internal pulls, then writes canonical refs back to gordon_domain. Remote CLI and admin API traffic should use gordon_domain.

Without this migration, a Host/remote-target mismatch can break routing or remote CLI token exchange.

When requests arrive on an HTTP-capable edge entrypoint with this domain as the Host header, Gordon routes them to the backend services (registry and admin API).

Security note:

  • Direct access to /admin/* on registry_port is blocked for non-loopback clients.
  • Admin API traffic should go through an HTTP-capable edge entrypoint such as entrypoints.edge with protocol = "smart_tcp".

Note: registry_domain is a deprecated migration key; use gordon_domain for new and upgraded configs.

Data Directory

The data_dir setting controls where Gordon stores data:

[server]
data_dir = "~/.gordon"  # Default for user installations

Directory Structure

~/.gordon/
├── registry/           # Container images and manifests
│   ├── blobs/
│   └── manifests/
├── logs/               # Gordon process and file-based access logs
│   ├── gordon.log
│   └── access.log
└── secrets/            # Secrets (unsafe backend only)

Permissions

Gordon creates directories with secure permissions:

  • Directories: 0700 (owner only)
  • Files: 0600 (owner only)

Maximum Proxy Body Size

The max_proxy_body_size setting limits the size of request bodies that Gordon will forward to backend containers:

[server]
max_proxy_body_size = "512MB"  # Default

Purpose: Prevents resource exhaustion attacks from extremely large uploads through the proxy.

Supported units: B, KB, MB, GB, TB (case-insensitive, decimal/1024-based)

Examples:

max_proxy_body_size = "100MB"   # Stricter limit
max_proxy_body_size = "1GB"     # Allow larger uploads
max_proxy_body_size = "0"       # No limit (not recommended)

Behavior:

  • When a request exceeds the limit, Gordon returns 413 Request Entity Too Large
  • The limit is applied before the request reaches the backend container
  • Uploads in progress are terminated when the limit is exceeded

Use cases:

  • File upload services: increase to 1GB or higher
  • CI/CD artifact uploads: adjust based on your largest artifacts
  • Tight security: reduce to 100MB or lower
  • Default (512MB): suitable for most applications

Warning: Setting this to 0 (no limit) allows unlimited upload sizes, which can lead to disk exhaustion and DoS vulnerabilities. Always set a reasonable limit for production deployments.

Maximum Registry Blob Chunk Size

The max_blob_chunk_size setting limits the size of a single blob upload request to the registry (PATCH/PUT under /v2/*/blobs/uploads/*):

[server]
max_blob_chunk_size = "95MB"  # Default

Purpose: Prevents excessive memory usage during blob uploads while allowing large real-world image layers.

Supported units: B, KB, MB, GB, TB (case-insensitive, decimal/1024-based)

Examples:

max_blob_chunk_size = "256MB"  # Stricter limit
max_blob_chunk_size = "1GB"    # Allow larger layers

Behavior:

  • When a blob chunk exceeds the limit, Gordon returns 413 Request Entity Too Large
  • Gordon CLI uploads image layers in 50MB chunks by default, and this setting caps the maximum chunk accepted per request
  • Keep this below Cloudflare's 100MB per-request limit when operating behind Cloudflare; the default (95MB) is fine
  • Increase it only if you expect larger direct registry uploads from other clients

Maximum Registry Blob Size

The max_blob_size setting limits the cumulative size of one blob/layer upload across all chunks:

[server]
max_blob_size = "1GB"  # Default

Purpose: Prevents disk exhaustion from multi-chunk uploads where each individual chunk is below max_blob_chunk_size but the total blob grows without bound.

Behavior:

  • Gordon tracks the current upload size and rejects appends that would exceed the limit.
  • Rejected uploads return an OCI-compatible 413 size error.
  • Failed uploads are cleaned up so temporary blob data does not accumulate indefinitely.

Registry IP Allowlist

The registry_allowed_ips setting restricts registry access to specific IPs or IP ranges. Accepts both CIDR notation (100.64.0.0/10) and individual IPs (203.0.113.50). When set, only requests from listed addresses (plus localhost) can reach registry and auth endpoints. An empty list allows all traffic (default).

[server]
registry_allowed_ips = [
  "100.64.0.0/10",   # Tailscale CGNAT range
  "203.0.113.50",    # single IP (treated as /32)
]

Behavior:

  • Single IPs are automatically converted to /32 (IPv4) or /128 (IPv6)
  • Denied requests receive 403 Forbidden
  • Localhost (127.0.0.0/8, ::1) is always allowed so Gordon can pull from its own registry
  • Respects trusted_proxies for accurate client IP extraction behind reverse proxies
  • Applied before rate limiting and authentication
  • Changes require a Gordon server restart; not picked up by config hot-reload

Use cases:

  • Restrict registry to Tailscale network: ["100.64.0.0/10"]
  • Restrict to private subnets: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
  • Allow a specific CI server: ["100.64.0.0/10", "203.0.113.50"]

Via environment variable:

GORDON_SERVER_REGISTRY_ALLOWED_IPS=100.64.0.0/10

Proxy Origin IP Allowlist

The proxy_allowed_ips setting restricts which IPs can reach the proxy server. This prevents direct-to-origin attacks that bypass CDN protections (e.g. Cloudflare WAF, DDoS mitigation). When set, only connections from listed CIDRs (plus localhost) are accepted.

[server]
# Only accept proxy traffic from Cloudflare edge IPs
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",
]

Behavior:

  • Checks the direct connection IP (RemoteAddr), not X-Forwarded-For
  • Localhost is always allowed
  • Denied requests receive 403 Forbidden
  • An empty list allows all traffic (default)

Important: Cloudflare publishes their IP ranges at cloudflare.com/ips. Keep your allowlist updated when Cloudflare adds new ranges.

Client IP Headers

Gordon sets the client IP on every request it proxies to a container. Apps can trust these headers without any extra proxy in front, as long as the container is only reachable through Gordon (no published host ports):

Header Value sent to the container
X-Real-IP The resolved client IP. Any value sent by the client is replaced, including X_Real_IP-style spellings.
X-Forwarded-For The same single resolved client IP.
Cf-Connecting-Ip Kept only when the peer is a trusted Cloudflare address and the value is the resolved client IP, otherwise removed.
X-Forwarded-Proto / X-Forwarded-Host Request scheme and route host. A trusted proxy's X-Forwarded-Proto is kept.

The client IP is resolved with the same rules as Gordon's logs and rate limiter, using api.rate_limit.trusted_proxies:

  • Untrusted peer: the peer address.
  • Trusted Cloudflare peer: a valid Cf-Connecting-Ip, otherwise the trusted proxy rules below.
  • Other trusted proxy: the right-most untrusted X-Forwarded-For hop (the left-most hop if all are trusted), then a single X-Real-IP, then the peer address.

IPv4-mapped IPv6 addresses are sent as plain IPv4. For apps that read the client IP from a header (for example ADDRESS_HEADER=x-real-ip in SvelteKit's adapter-node), use X-Real-IP.

Registry Listen Address

The registry_listen_address controls which interface the registry server binds to. By default it binds to all interfaces. Set to 127.0.0.1 to restrict registry access to localhost only, preventing containers from reaching the registry via the Docker bridge gateway.

[server]
registry_listen_address = "127.0.0.1"  # Loopback only

HSTS

HSTS headers are sent automatically on TLS requests (when r.TLS != nil). When Gordon runs behind a TLS-terminating proxy (Cloudflare, Tailscale) and only receives plain HTTP, HSTS is not sent — the edge proxy is expected to handle it.

Examples

Development Configuration

[server]
registry_port = 5000
gordon_domain = "gordon.local"
data_dir = "./dev-data"

[entrypoints.edge]
address = ":9000"
protocol = "smart_tcp"

Production Configuration

[server]
registry_port = 5000
gordon_domain = "gordon.company.com"
data_dir = "/var/lib/gordon"

[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"

Rootless Container Setup

[server]
registry_port = 5000
gordon_domain = "gordon.mydomain.com"

[entrypoints.edge]
address = ":9000"      # High port for rootless
protocol = "smart_tcp"

With firewall port forwarding:

sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
sudo firewall-cmd --reload
source
1# Server Configuration
2
3Core server settings for Gordon.
4
5## Configuration
6
7```toml
8[server]
9registry_port = 5000                     # Container registry port
10gordon_domain = "gordon.mydomain.com"    # Gordon domain (required)
11# tls_cert_file = ""                       # Optional: PEM certificate for static TLS
12# tls_key_file = ""                        # Optional: PEM key for static TLS
13# force_https_redirect = false             # Redirect cleartext HTTP requests to HTTPS
14# legacy_registry_domains = ["registry.example.com:5000"]
15# data_dir = "~/.gordon"                 # Data storage directory (default)
16max_blob_chunk_size = "95MB"             # Max registry blob upload chunk
17max_blob_size = "1GB"                    # Max cumulative registry blob/layer upload
18
19[entrypoints.edge]
20address = ":443"                         # Deployment-selected public TCP socket
21protocol = "smart_tcp"
22```
23
24## Options
25
26| Option | Type | Default | Description |
27|--------|------|---------|-------------|
28| `registry_port` | int | `5000` | Docker registry port for image push/pull |
29| `tls_cert_file` | string | `""` | Path to PEM certificate file. When set with `tls_key_file`, matching SNI domains use this static cert before ACME or internal CA certs |
30| `tls_key_file` | string | `""` | Path to PEM private key file. Must be set together with `tls_cert_file` |
31| `force_https_redirect` | bool | `false` | Redirect cleartext HTTP requests to HTTPS on HTTP-capable edge entrypoints |
32| `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) |
33| `registry_domain` | string | - | Deprecated migration key. Set `gordon_domain` instead. |
34| `legacy_registry_domains` | []string | `[]` | Additional Gordon registry hosts treated as aliases during staged migration. See [Upgrading: Staged Registry Host Rename](../upgrading.md#staged-registry-host-rename). |
35| `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and state |
36| `max_proxy_body_size` | string | `"512MB"` | Maximum request body size for proxied requests |
37| `max_blob_chunk_size` | string | `"95MB"` | Maximum request body size for a single registry blob upload chunk |
38| `max_blob_size` | string | `"1GB"` | Maximum cumulative size for one registry blob/layer upload |
39| `registry_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
40| `proxy_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to reach HTTP proxy paths (empty = allow all) |
41| `registry_listen_address` | string | `""` | Bind address for registry (empty = all interfaces) |
42
43Public application traffic is configured under `[entrypoints]`. The standard edge listener is a `smart_tcp` socket:
44
45```toml
46[entrypoints.edge]
47address = ":443"
48protocol = "smart_tcp"
49```
50
51`entrypoints.edge.address` is not an HTTP or HTTPS port. It is the TCP address Gordon sniffs for cleartext HTTP, h2c, TLS passthrough, normal HTTPS fallback, or explicit raw TCP fallback. Gordon does not provide a built-in default public port; choose `:443`, a high port such as `:9000`, or container/firewall mappings to match your deployment.
52
53## Registry HTTP Timeouts
54
55The registry listeners (`registry_port`, plus the loopback listener used for the internal registry) apply fixed per-connection timeouts. Values are Go duration strings; malformed, zero, or negative values are rejected when the configuration loads.
56
57```toml
58[server.http]
59read_header_timeout = "10s"
60read_timeout = "5m"
61write_timeout = "5m"
62idle_timeout = "120s"
63```
64
65| Option | Env | Default | Description |
66|--------|-----|---------|-------------|
67| `server.http.read_header_timeout` | `GORDON_SERVER_HTTP_READ_HEADER_TIMEOUT` | `"10s"` | Maximum time to read request headers |
68| `server.http.read_timeout` | `GORDON_SERVER_HTTP_READ_TIMEOUT` | `"5m"` | Maximum time to read the whole request, body included |
69| `server.http.write_timeout` | `GORDON_SERVER_HTTP_WRITE_TIMEOUT` | `"5m"` | Maximum time to write the whole response |
70| `server.http.idle_timeout` | `GORDON_SERVER_HTTP_IDLE_TIMEOUT` | `"120s"` | Maximum keep-alive idle time between requests |
71
72`read_timeout` and `write_timeout` bound a whole registry request or response, so a layer transfer longer than these values is cut even while data flows. Raise them for very large layers over slow links. Traffic behind entrypoints (apps and the admin API) uses an inactivity timeout instead; see [Traffic HTTP Timeouts](./traffic.md#http-timeouts). The owner-only local admin socket has no write timeout so log streaming stays open. Changes require a restart.
73
74## Internal CA and TLS
75
76Gordon includes an internal certificate authority that issues on-demand TLS certificates for proxied domains served through HTTPS fallback on a TLS-capable entrypoint such as `smart_tcp`.
77
78With TLS-capable edge traffic:
79- Gordon runs an internal CA (root + intermediate) stored in `{data_dir}/pki/`
80- Leaf certificates are issued on-demand per domain (SNI-based) and cached in memory
81- The intermediate CA auto-renews before expiry
82- An onboarding page at `https://<gordon-host>/.well-known/gordon/` lets clients download the root CA certificate
83- The root CA is stable across restarts (generated once, persisted to disk)
84
85#### Custom certificates
86
87To use your own certificate (e.g. from Tailscale, Let's Encrypt, or a corporate CA) alongside ACME/internal CA fallback:
88
89```toml
90[server]
91tls_cert_file = "/etc/gordon/tls/cert.pem"
92tls_key_file = "/etc/gordon/tls/key.pem"
93```
94
95Certificate priority for normal HTTPS fallback is: static certificates first, then public ACME certificates, then Gordon's internal CA.
96
97#### Public ACME certificates
98
99Gordon can obtain public certificates through Let's Encrypt-compatible ACME when `[tls.acme]` is enabled. Normal HTTPS fallback must be available on a TLS-capable entrypoint.
100
101```toml
102[entrypoints.edge]
103address = ":443"
104protocol = "smart_tcp"
105
106[dns]
107resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
108propagation_timeout = "5m"
109polling_interval = "5s"
110
111[tls.acme]
112enabled = true
113email = "admin@example.com"
114challenge = "auto"       # auto, http-01, or cloudflare-dns-01
115obtain_batch_size = 1    # maximum new certificate orders per reconcile run
116```
117
118Challenge behavior:
119- `cloudflare-dns-01` uses a Cloudflare API token from `pass`, `GORDON_CLOUDFLARE_API_TOKEN_FILE`, or `GORDON_CLOUDFLARE_API_TOKEN`; it does not need a special public port 80 edge.
120- `http-01` serves `/.well-known/acme-challenge/<token>` through Gordon's HTTP handler and requires an HTTP-capable `smart_tcp` entrypoint reachable on external port 80 for each hostname being validated.
121- `auto` selects Cloudflare DNS-01 when a token is available, otherwise falls back to HTTP-01.
122- `tls-alpn-01` is not supported.
123
124HTTP-01 requires public access to external port 80 for each hostname being validated. If you use Cloudflare in DNS-only/gray-cloud mode, your firewall/NAT must allow direct public traffic to the HTTP-capable smart TCP entrypoint. A firewall rule that only allows Cloudflare source IPs on port 80 is compatible with orange-cloud proxying, but it blocks gray-cloud HTTP-01 validation; use DNS-01 or temporarily open port 80 for direct validation.
125
126Gordon automatically includes `server.gordon_domain` in public ACME coverage in addition to app HTTPS hosts. The management hostname therefore needs the same challenge reachability: public port 80 for HTTP-01, or zone-read and DNS-edit permissions for its zone with Cloudflare DNS-01.
127
128Gordon limits new ACME certificate orders to `obtain_batch_size` per reconcile run (default `1`) so enabling ACME on an existing multi-route server does not burst through every route and hit Let's Encrypt rate limits. The management hostname consumes a place in the same batch; later reloads, restarts, or other explicit reconcile runs continue issuing remaining certificates.
129
130If the initial ACME reconcile fails (e.g. due to a transient network error or misconfiguration), Gordon logs the failure and keeps serving any existing certificates. The renewal loop retries reconcile work periodically, so missing certificates self-heal after the underlying issue is fixed without requiring a restart.
131
132For Cloudflare Full/Strict, Cloudflare terminates browser TLS at the edge and connects to Gordon over HTTPS. A public ACME certificate served by Gordon is the preferred origin certificate because Cloudflare Strict can validate it without custom origin trust. Cloudflare Flexible mode (HTTPS at the edge, HTTP to Gordon) is not end-to-end HTTPS.
133
134Static certificates have priority, then public ACME certificates, then Gordon's internal CA fallback. Gordon uses the `go-acme/lego` ACME client, including its DNS-provider support for Cloudflare DNS-01.
135
136DNS-01 uses the Cloudflare API to create TXT records, then checks public DNS visibility through `[dns].resolvers`. These resolvers are recursive resolvers (for example Cloudflare DNS or Google DNS), not the authoritative DNS provider. A domain can be hosted at Cloudflare while Gordon verifies propagation through Google DNS.
137
138The defaults avoid relying on host-local DNS. This matters on hosts using Tailscale MagicDNS, split-horizon corporate DNS, or Pi-hole, where `/etc/resolv.conf` may not reflect public DNS visibility as Let's Encrypt sees it.
139
140Because Gordon's ACME challenge mode is global, `cloudflare-dns-01` requires a Cloudflare token that can read zones and edit DNS records for every zone used by app HTTPS hosts. If that is not desirable, use `http-01` until Gordon supports per-route or per-zone challenge policy.
141
142#### Direct HTTP CA onboarding paths (`/.well-known/gordon/ca`)
143
144When Gordon is serving TLS-capable edge traffic, direct cleartext HTTP clients (those not arriving through a trusted proxy) are restricted to CA onboarding paths only:
145
146- `/.well-known/gordon/`, `/.well-known/gordon/ca`, `/.well-known/gordon/ca.crt`, `/.well-known/gordon/ca.mobileconfig` — serve the onboarding page and CA certificate downloads
147- `/.well-known/acme-challenge/*` — serves ACME HTTP-01 challenges when public ACME HTTP-01 is enabled, otherwise returns `404 Not Found`
148- All other paths return `403 Forbidden`
149
150This lets new clients discover and trust the internal CA over plain HTTP without exposing the full application. Trusted proxy traffic (e.g. from Cloudflare) continues through the normal HTTP proxy path unaffected.
151
152On HTTPS, onboarding paths are served only on `server.gordon_domain`; app hosts are served without Gordon intercepting them. If `gordon_domain` is empty, HTTPS onboarding paths are not served at all to avoid intercepting app hosts.
153
154#### HTTP to HTTPS redirect
155
156When clients connect directly (no Cloudflare or reverse proxy in front), enable `force_https_redirect` to redirect cleartext HTTP traffic to HTTPS on the same smart TCP edge.
157
158```toml
159[server]
160force_https_redirect = true
161```
162
163When `proxy_allowed_ips` is configured, non-proxy clients are redirected automatically even without this flag — trusted proxy IPs pass through to serve Cloudflare-proxied traffic.
164
165#### Firewall and Container Port Mapping
166
167Choose the bind address in `[entrypoints.edge]` and map external ports to it as needed. For example, a rootless service can bind a high port while the firewall exposes 443:
168
169```toml
170[entrypoints.edge]
171address = ":9000"
172protocol = "smart_tcp"
173```
174
175```bash
176sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
177sudo firewall-cmd --reload
178```
179
180`:443`, `:9000`, and container `-p 443:9000` mappings are deployment choices, not protocol modes.
181
182### Registry Port
183
184The `registry_port` setting controls where Gordon accepts image pushes:
185
186```toml
187[server]
188registry_port = 5000
189```
190
191Docker/Podman clients push to this port:
192
193```bash
194docker push registry.mydomain.com:5000/myapp:latest
195```
196
197If the registry is served over plain HTTP (no TLS), clients must be configured to allow insecure access. See [Troubleshooting: insecure registry](../reference/troubleshooting.md#server-gave-http-response-to-https-client) for Docker and Podman instructions.
198
199## Gordon Domain
200
201The `gordon_domain` is required and must match your DNS configuration:
202
203```toml
204[server]
205gordon_domain = "gordon.mydomain.com"
206```
207
208This domain is used for:
209- Docker login and image push/pull operations
210- Admin API access (`/admin/*` endpoints)
211- CLI remote targeting (`gordon apps --remote https://gordon.mydomain.com`)
212- Authentication endpoints (`/auth/*`)
213
214> **Warning:** If you are upgrading an older config, copy `server.registry_domain` to `server.gordon_domain` before restarting.
215
216For a staged rename, set `gordon_domain` to the new public host and add any old Gordon registry hosts that clients still use to `legacy_registry_domains` (including `host:port` forms). Gordon treats those entries as registry aliases during image matching and internal pulls, then writes canonical refs back to `gordon_domain`. Remote CLI and admin API traffic should use `gordon_domain`.
217
218Without this migration, a Host/remote-target mismatch can break routing or remote CLI token exchange.
219
220When requests arrive on an HTTP-capable edge entrypoint with this domain as the Host header, Gordon routes them to the backend services (registry and admin API).
221
222Security note:
223- Direct access to `/admin/*` on `registry_port` is blocked for non-loopback clients.
224- Admin API traffic should go through an HTTP-capable edge entrypoint such as `entrypoints.edge` with `protocol = "smart_tcp"`.
225
226> **Note:** `registry_domain` is a deprecated migration key; use `gordon_domain` for new and upgraded configs.
227
228## Data Directory
229
230The `data_dir` setting controls where Gordon stores data:
231
232```toml
233[server]
234data_dir = "~/.gordon"  # Default for user installations
235```
236
237### Directory Structure
238
239```
240~/.gordon/
241├── registry/           # Container images and manifests
242│   ├── blobs/
243│   └── manifests/
244├── logs/               # Gordon process and file-based access logs
245│   ├── gordon.log
246│   └── access.log
247└── secrets/            # Secrets (unsafe backend only)
248```
249
250### Permissions
251
252 Gordon creates directories with secure permissions:
253 - Directories: `0700` (owner only)
254 - Files: `0600` (owner only)
255
256 ## Maximum Proxy Body Size
257
258 The `max_proxy_body_size` setting limits the size of request bodies that Gordon will forward to backend containers:
259
260 ```toml
261 [server]
262 max_proxy_body_size = "512MB"  # Default
263 ```
264
265 **Purpose:** Prevents resource exhaustion attacks from extremely large uploads through the proxy.
266
267 **Supported units:** B, KB, MB, GB, TB (case-insensitive, decimal/1024-based)
268
269 **Examples:**
270 ```toml
271 max_proxy_body_size = "100MB"   # Stricter limit
272 max_proxy_body_size = "1GB"     # Allow larger uploads
273 max_proxy_body_size = "0"       # No limit (not recommended)
274 ```
275
276 **Behavior:**
277 - When a request exceeds the limit, Gordon returns `413 Request Entity Too Large`
278 - The limit is applied before the request reaches the backend container
279 - Uploads in progress are terminated when the limit is exceeded
280
281 **Use cases:**
282 - File upload services: increase to `1GB` or higher
283 - CI/CD artifact uploads: adjust based on your largest artifacts
284 - Tight security: reduce to `100MB` or lower
285 - Default (512MB): suitable for most applications
286
287 > **Warning:** Setting this to `0` (no limit) allows unlimited upload sizes, which can lead to disk exhaustion and DoS vulnerabilities. Always set a reasonable limit for production deployments.
288
289## Maximum Registry Blob Chunk Size
290
291The `max_blob_chunk_size` setting limits the size of a single blob upload request to the registry (`PATCH`/`PUT` under `/v2/*/blobs/uploads/*`):
292
293```toml
294[server]
295max_blob_chunk_size = "95MB"  # Default
296```
297
298**Purpose:** Prevents excessive memory usage during blob uploads while allowing large real-world image layers.
299
300**Supported units:** B, KB, MB, GB, TB (case-insensitive, decimal/1024-based)
301
302**Examples:**
303
304```toml
305max_blob_chunk_size = "256MB"  # Stricter limit
306max_blob_chunk_size = "1GB"    # Allow larger layers
307```
308
309**Behavior:**
310- When a blob chunk exceeds the limit, Gordon returns `413 Request Entity Too Large`
311- Gordon CLI uploads image layers in 50MB chunks by default, and this setting caps the maximum chunk accepted per request
312- Keep this below Cloudflare's 100MB per-request limit when operating behind Cloudflare; the default (`95MB`) is fine
313- Increase it only if you expect larger direct registry uploads from other clients
314
315## Maximum Registry Blob Size
316
317The `max_blob_size` setting limits the cumulative size of one blob/layer upload across all chunks:
318
319```toml
320[server]
321max_blob_size = "1GB"  # Default
322```
323
324**Purpose:** Prevents disk exhaustion from multi-chunk uploads where each individual chunk is below `max_blob_chunk_size` but the total blob grows without bound.
325
326**Behavior:**
327- Gordon tracks the current upload size and rejects appends that would exceed the limit.
328- Rejected uploads return an OCI-compatible `413` size error.
329- Failed uploads are cleaned up so temporary blob data does not accumulate indefinitely.
330
331## Registry IP Allowlist
332
333The `registry_allowed_ips` setting restricts registry access to specific IPs or IP ranges. Accepts both CIDR notation (`100.64.0.0/10`) and individual IPs (`203.0.113.50`). When set, only requests from listed addresses (plus localhost) can reach registry and auth endpoints. An empty list allows all traffic (default).
334
335```toml
336[server]
337registry_allowed_ips = [
338  "100.64.0.0/10",   # Tailscale CGNAT range
339  "203.0.113.50",    # single IP (treated as /32)
340]
341```
342
343**Behavior:**
344- Single IPs are automatically converted to `/32` (IPv4) or `/128` (IPv6)
345- Denied requests receive `403 Forbidden`
346- Localhost (`127.0.0.0/8`, `::1`) is always allowed so Gordon can pull from its own registry
347- Respects `trusted_proxies` for accurate client IP extraction behind reverse proxies
348- Applied before rate limiting and authentication
349- Changes require a Gordon server restart; not picked up by config hot-reload
350
351**Use cases:**
352- Restrict registry to Tailscale network: `["100.64.0.0/10"]`
353- Restrict to private subnets: `["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]`
354- Allow a specific CI server: `["100.64.0.0/10", "203.0.113.50"]`
355
356Via environment variable:
357
358```bash
359GORDON_SERVER_REGISTRY_ALLOWED_IPS=100.64.0.0/10
360```
361
362## Proxy Origin IP Allowlist
363
364The `proxy_allowed_ips` setting restricts which IPs can reach the proxy server. This prevents direct-to-origin attacks that bypass CDN protections (e.g. Cloudflare WAF, DDoS mitigation). When set, only connections from listed CIDRs (plus localhost) are accepted.
365
366```toml
367[server]
368# Only accept proxy traffic from Cloudflare edge IPs
369proxy_allowed_ips = [
370  "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
371  "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
372  "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
373  "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
374  "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
375]
376```
377
378**Behavior:**
379- Checks the direct connection IP (`RemoteAddr`), not `X-Forwarded-For`
380- Localhost is always allowed
381- Denied requests receive `403 Forbidden`
382- An empty list allows all traffic (default)
383
384**Important:** Cloudflare publishes their IP ranges at [cloudflare.com/ips](https://www.cloudflare.com/ips/). Keep your allowlist updated when Cloudflare adds new ranges.
385
386## Client IP Headers
387
388Gordon sets the client IP on every request it proxies to a container. Apps can trust these headers without any extra proxy in front, as long as the container is only reachable through Gordon (no published host ports):
389
390| Header | Value sent to the container |
391|--------|-----------------------------|
392| `X-Real-IP` | The resolved client IP. Any value sent by the client is replaced, including `X_Real_IP`-style spellings. |
393| `X-Forwarded-For` | The same single resolved client IP. |
394| `Cf-Connecting-Ip` | Kept only when the peer is a trusted Cloudflare address and the value is the resolved client IP, otherwise removed. |
395| `X-Forwarded-Proto` / `X-Forwarded-Host` | Request scheme and route host. A trusted proxy's `X-Forwarded-Proto` is kept. |
396
397The client IP is resolved with the same rules as Gordon's logs and rate limiter, using [`api.rate_limit.trusted_proxies`](./rate-limiting.md#trusted_proxies):
398
399- **Untrusted peer:** the peer address.
400- **Trusted Cloudflare peer:** a valid `Cf-Connecting-Ip`, otherwise the trusted proxy rules below.
401- **Other trusted proxy:** the right-most untrusted `X-Forwarded-For` hop (the left-most hop if all are trusted), then a single `X-Real-IP`, then the peer address.
402
403IPv4-mapped IPv6 addresses are sent as plain IPv4. For apps that read the client IP from a header (for example `ADDRESS_HEADER=x-real-ip` in SvelteKit's adapter-node), use `X-Real-IP`.
404
405## Registry Listen Address
406
407The `registry_listen_address` controls which interface the registry server binds to. By default it binds to all interfaces. Set to `127.0.0.1` to restrict registry access to localhost only, preventing containers from reaching the registry via the Docker bridge gateway.
408
409```toml
410[server]
411registry_listen_address = "127.0.0.1"  # Loopback only
412```
413
414## HSTS
415
416HSTS headers are sent automatically on TLS requests (when `r.TLS != nil`). When Gordon runs behind a TLS-terminating proxy (Cloudflare, Tailscale) and only receives plain HTTP, HSTS is not sent — the edge proxy is expected to handle it.
417
418## Examples
419
420### Development Configuration
421
422```toml
423[server]
424registry_port = 5000
425gordon_domain = "gordon.local"
426data_dir = "./dev-data"
427
428[entrypoints.edge]
429address = ":9000"
430protocol = "smart_tcp"
431```
432
433### Production Configuration
434
435```toml
436[server]
437registry_port = 5000
438gordon_domain = "gordon.company.com"
439data_dir = "/var/lib/gordon"
440
441[entrypoints.edge]
442address = ":443"
443protocol = "smart_tcp"
444```
445
446### Rootless Container Setup
447
448```toml
449[server]
450registry_port = 5000
451gordon_domain = "gordon.mydomain.com"
452
453[entrypoints.edge]
454address = ":9000"      # High port for rootless
455protocol = "smart_tcp"
456```
457
458With firewall port forwarding:
459
460```bash
461sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
462sudo firewall-cmd --reload
463```
464
465## Related
466
467- [Configuration Overview](./index.md)
468- [Installation Guide](../installation.md)
469- [App Manifest](./apps.md)
470- [Standalone Services](./services.md)
471- [Traffic Plane Configuration](./traffic.md)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close