v2.31.0-dev18 7b9923f

server.md RawHistoryFolder

Server Configuration

Core server settings for Gordon.

Configuration

[server]
port = 8088                              # HTTP proxy port
registry_port = 5000                     # Container registry port
tls_port = 8443                          # HTTPS listener port (0 = disabled)
# tls_cert_file = ""                       # Optional: PEM certificate for static TLS
# tls_key_file = ""                        # Optional: PEM key for static TLS
# force_https_redirect = false             # Redirect all HTTP traffic to HTTPS
gordon_domain = "gordon.mydomain.com"    # Gordon domain (required)
# 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

Options

Option Type Default Description
port int 8088 HTTP proxy port for routing traffic to containers
registry_port int 5000 Docker registry port for image push/pull
tls_port int 8443 HTTPS listener port with internal CA. Set to 0 to disable TLS entirely
tls_cert_file string "" Path to PEM certificate file. When set (with tls_key_file), this cert is served for matching SNI domains; unmatched domains fall through to the internal CA
tls_key_file string "" Path to PEM private key file. Must be set together with tls_cert_file
force_https_redirect bool false Redirect all HTTP requests to the HTTPS port. For direct-access setups without a TLS-terminating proxy
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 env files
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 the proxy (empty = allow all)
registry_listen_address string "" Bind address for registry (empty = all interfaces)

Port Configuration

Proxy Port

The port setting controls where Gordon listens for HTTP traffic:

[server]
port = 8080  # Use 8080 for rootless containers with port forwarding

For rootless containers, you'll typically use a high port and configure firewall port forwarding:

# Forward port 80 to 8080
sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080

Internal CA and TLS

Gordon includes an internal certificate authority that issues on-demand TLS certificates for proxied domains. The HTTPS listener is enabled by default on tls_port (8443).

[server]
tls_port = 8443   # Default. Set to 0 to disable TLS entirely.

With TLS enabled:

  • 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>:<tls_port>/.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 the internal CA:

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

The static certificate is served for SNI-matching domains (including wildcards). All other domains get on-demand certs from the internal CA.

Public ACME certificates

Gordon can obtain public certificates through Let's Encrypt-compatible ACME when [tls.acme] is enabled. The HTTPS listener must also be enabled (server.tls_port > 0).

[server]
tls_port = 8443

[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:

  • http-01 serves /.well-known/acme-challenge/<token> on the HTTP proxy port.
  • cloudflare-dns-01 uses a Cloudflare API token from pass, GORDON_CLOUDFLARE_API_TOKEN_FILE, or GORDON_CLOUDFLARE_API_TOKEN.
  • auto selects Cloudflare DNS-01 when a token is available, otherwise falls back to HTTP-01.

HTTP-01 requires public access to 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 Gordon's HTTP proxy port. 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 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. 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 but still starts the renewal loop for already cached certificates. Operators should fix the underlying error and restart/reload Gordon to trigger a new obtain attempt for missing certificates.

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 configured HTTPS routes. If that is not desirable, use http-01 until Gordon supports per-route or per-zone challenge policy.

Direct HTTP Onboarding

When tls_port is non-zero, direct 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 can use their own routes 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 all HTTP traffic to the HTTPS port. Direct HTTP onboarding paths are still served even with this flag enabled.

[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.

Disabling TLS

For setups where TLS is handled entirely by an external proxy (Cloudflare, nginx, etc.):

[server]
tls_port = 0   # No HTTPS listener, no internal CA generated

Firewall port forwarding

For rootless user services, forward 443 to the high port at the firewall:

# Forward 443 -> 8443
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
sudo firewall-cmd --reload

With firewalld and Tailscale:

# Verify tailscale0 is in trusted zone
sudo firewall-cmd --get-zone-of-interface=tailscale0

# If needed, place tailscale0 in trusted
sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0
sudo firewall-cmd --reload

# Forward 443 -> 8443 permanently (IPv4 + IPv6)
sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv4 forward-port port=443 protocol=tcp to-port=8443'
sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv6 forward-port port=443 protocol=tcp to-port=8443'
sudo firewall-cmd --reload

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 routes --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 the proxy port 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 the main proxy listener (server.port/server.tls_port).

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/
├── env/                # Environment files per domain
│   ├── app_mydomain_com.env
│   └── api_mydomain_com.env
├── logs/               # Application and container logs
│   ├── gordon.log
│   ├── proxy.log
│   └── containers/
└── 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.

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 the HTTPS listener (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]
port = 8080
registry_port = 5000
gordon_domain = "gordon.local"
data_dir = "./dev-data"

Production Configuration

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

Rootless Container Setup

[server]
port = 8080      # High port for rootless
registry_port = 5000
gordon_domain = "gordon.mydomain.com"

With firewall port forwarding:

sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
sudo firewall-cmd --reload
source
1# Server Configuration
2
3Core server settings for Gordon.
4
5## Configuration
6
7```toml
8[server]
9port = 8088                              # HTTP proxy port
10registry_port = 5000                     # Container registry port
11tls_port = 8443                          # HTTPS listener port (0 = disabled)
12# tls_cert_file = ""                       # Optional: PEM certificate for static TLS
13# tls_key_file = ""                        # Optional: PEM key for static TLS
14# force_https_redirect = false             # Redirect all HTTP traffic to HTTPS
15gordon_domain = "gordon.mydomain.com"    # Gordon domain (required)
16# legacy_registry_domains = ["registry.example.com:5000"]
17# data_dir = "~/.gordon"                 # Data storage directory (default)
18max_blob_chunk_size = "95MB"             # Max registry blob upload chunk
19max_blob_size = "1GB"                    # Max cumulative registry blob/layer upload
20```
21
22## Options
23
24| Option | Type | Default | Description |
25|--------|------|---------|-------------|
26| `port` | int | `8088` | HTTP proxy port for routing traffic to containers |
27| `registry_port` | int | `5000` | Docker registry port for image push/pull |
28| `tls_port` | int | `8443` | HTTPS listener port with internal CA. Set to `0` to disable TLS entirely |
29| `tls_cert_file` | string | `""` | Path to PEM certificate file. When set (with `tls_key_file`), this cert is served for matching SNI domains; unmatched domains fall through to the internal CA |
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 all HTTP requests to the HTTPS port. For direct-access setups without a TLS-terminating proxy |
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 env files |
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 the proxy (empty = allow all) |
41| `registry_listen_address` | string | `""` | Bind address for registry (empty = all interfaces) |
42
43## Port Configuration
44
45### Proxy Port
46
47The `port` setting controls where Gordon listens for HTTP traffic:
48
49```toml
50[server]
51port = 8080  # Use 8080 for rootless containers with port forwarding
52```
53
54For rootless containers, you'll typically use a high port and configure firewall port forwarding:
55
56```bash
57# Forward port 80 to 8080
58sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
59```
60
61### Internal CA and TLS
62
63Gordon includes an internal certificate authority that issues on-demand TLS certificates for proxied domains. The HTTPS listener is enabled by default on `tls_port` (8443).
64
65```toml
66[server]
67tls_port = 8443   # Default. Set to 0 to disable TLS entirely.
68```
69
70With TLS enabled:
71- Gordon runs an internal CA (root + intermediate) stored in `{data_dir}/pki/`
72- Leaf certificates are issued on-demand per domain (SNI-based) and cached in memory
73- The intermediate CA auto-renews before expiry
74- An onboarding page at `https://<gordon-host>:<tls_port>/.well-known/gordon/` lets clients download the root CA certificate
75- The root CA is stable across restarts (generated once, persisted to disk)
76
77#### Custom certificates
78
79To use your own certificate (e.g. from Tailscale, Let's Encrypt, or a corporate CA) alongside the internal CA:
80
81```toml
82[server]
83tls_cert_file = "/etc/gordon/tls/cert.pem"
84tls_key_file = "/etc/gordon/tls/key.pem"
85```
86
87The static certificate is served for SNI-matching domains (including wildcards). All other domains get on-demand certs from the internal CA.
88
89#### Public ACME certificates
90
91Gordon can obtain public certificates through Let's Encrypt-compatible ACME when `[tls.acme]` is enabled. The HTTPS listener must also be enabled (`server.tls_port > 0`).
92
93```toml
94[server]
95tls_port = 8443
96
97[dns]
98resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
99propagation_timeout = "5m"
100polling_interval = "5s"
101
102[tls.acme]
103enabled = true
104email = "admin@example.com"
105challenge = "auto"       # auto, http-01, or cloudflare-dns-01
106obtain_batch_size = 1    # maximum new certificate orders per reconcile run
107```
108
109Challenge behavior:
110- `http-01` serves `/.well-known/acme-challenge/<token>` on the HTTP proxy port.
111- `cloudflare-dns-01` uses a Cloudflare API token from `pass`, `GORDON_CLOUDFLARE_API_TOKEN_FILE`, or `GORDON_CLOUDFLARE_API_TOKEN`.
112- `auto` selects Cloudflare DNS-01 when a token is available, otherwise falls back to HTTP-01.
113
114HTTP-01 requires public access to 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 Gordon's HTTP proxy port. 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.
115
116Gordon 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. Later reloads, restarts, or other explicit reconcile runs continue issuing remaining certificates.
117
118If the initial ACME reconcile fails (e.g. due to a transient network error or misconfiguration), Gordon logs the failure but still starts the renewal loop for already cached certificates. Operators should fix the underlying error and restart/reload Gordon to trigger a new obtain attempt for missing certificates.
119
120For 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.
121
122Static 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.
123
124DNS-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.
125
126The 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.
127
128Because 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 configured HTTPS routes. If that is not desirable, use `http-01` until Gordon supports per-route or per-zone challenge policy.
129
130#### Direct HTTP Onboarding
131
132When `tls_port` is non-zero, direct HTTP clients (those not arriving through a trusted proxy) are restricted to CA onboarding paths only:
133
134- `/.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
135- `/.well-known/acme-challenge/*` — serves ACME HTTP-01 challenges when public ACME HTTP-01 is enabled, otherwise returns `404 Not Found`
136- All other paths return `403 Forbidden`
137
138This 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.
139
140On HTTPS, onboarding paths are served only on `server.gordon_domain`; app hosts can use their own routes without Gordon intercepting them. If `gordon_domain` is empty, HTTPS onboarding paths are not served at all to avoid intercepting app hosts.
141
142#### HTTP to HTTPS redirect
143
144When clients connect directly (no Cloudflare or reverse proxy in front), enable `force_https_redirect` to redirect all HTTP traffic to the HTTPS port. Direct HTTP onboarding paths are still served even with this flag enabled.
145
146```toml
147[server]
148force_https_redirect = true
149```
150
151When `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.
152
153#### Disabling TLS
154
155For setups where TLS is handled entirely by an external proxy (Cloudflare, nginx, etc.):
156
157```toml
158[server]
159tls_port = 0   # No HTTPS listener, no internal CA generated
160```
161
162#### Firewall port forwarding
163
164For rootless user services, forward 443 to the high port at the firewall:
165
166```bash
167# Forward 443 -> 8443
168sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
169sudo firewall-cmd --reload
170```
171
172With firewalld and Tailscale:
173
174```bash
175# Verify tailscale0 is in trusted zone
176sudo firewall-cmd --get-zone-of-interface=tailscale0
177
178# If needed, place tailscale0 in trusted
179sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0
180sudo firewall-cmd --reload
181
182# Forward 443 -> 8443 permanently (IPv4 + IPv6)
183sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv4 forward-port port=443 protocol=tcp to-port=8443'
184sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv6 forward-port port=443 protocol=tcp to-port=8443'
185sudo firewall-cmd --reload
186```
187
188### Registry Port
189
190The `registry_port` setting controls where Gordon accepts image pushes:
191
192```toml
193[server]
194registry_port = 5000
195```
196
197Docker/Podman clients push to this port:
198
199```bash
200docker push registry.mydomain.com:5000/myapp:latest
201```
202
203If 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.
204
205## Gordon Domain
206
207The `gordon_domain` is required and must match your DNS configuration:
208
209```toml
210[server]
211gordon_domain = "gordon.mydomain.com"
212```
213
214This domain is used for:
215- Docker login and image push/pull operations
216- Admin API access (`/admin/*` endpoints)
217- CLI remote targeting (`gordon routes --remote https://gordon.mydomain.com`)
218- Authentication endpoints (`/auth/*`)
219
220> **Warning:** If you are upgrading an older config, copy `server.registry_domain` to `server.gordon_domain` before restarting.
221
222For 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`.
223
224Without this migration, a Host/remote-target mismatch can break routing or remote CLI token exchange.
225
226When requests arrive on the proxy port with this domain as the Host header, Gordon routes them to the backend services (registry and admin API).
227
228Security note:
229- Direct access to `/admin/*` on `registry_port` is blocked for non-loopback clients.
230- Admin API traffic should go through the main proxy listener (`server.port`/`server.tls_port`).
231
232> **Note:** `registry_domain` is a deprecated migration key; use `gordon_domain` for new and upgraded configs.
233
234## Data Directory
235
236The `data_dir` setting controls where Gordon stores data:
237
238```toml
239[server]
240data_dir = "~/.gordon"  # Default for user installations
241```
242
243### Directory Structure
244
245```
246~/.gordon/
247├── registry/           # Container images and manifests
248│   ├── blobs/
249│   └── manifests/
250├── env/                # Environment files per domain
251│   ├── app_mydomain_com.env
252│   └── api_mydomain_com.env
253├── logs/               # Application and container logs
254│   ├── gordon.log
255│   ├── proxy.log
256│   └── containers/
257└── secrets/            # Secrets (unsafe backend only)
258```
259
260### Permissions
261
262 Gordon creates directories with secure permissions:
263 - Directories: `0700` (owner only)
264 - Files: `0600` (owner only)
265
266 ## Maximum Proxy Body Size
267
268 The `max_proxy_body_size` setting limits the size of request bodies that Gordon will forward to backend containers:
269
270 ```toml
271 [server]
272 max_proxy_body_size = "512MB"  # Default
273 ```
274
275 **Purpose:** Prevents resource exhaustion attacks from extremely large uploads through the proxy.
276
277 **Supported units:** B, KB, MB, GB, TB (case-insensitive, decimal/1024-based)
278
279 **Examples:**
280 ```toml
281 max_proxy_body_size = "100MB"   # Stricter limit
282 max_proxy_body_size = "1GB"     # Allow larger uploads
283 max_proxy_body_size = "0"       # No limit (not recommended)
284 ```
285
286 **Behavior:**
287 - When a request exceeds the limit, Gordon returns `413 Request Entity Too Large`
288 - The limit is applied before the request reaches the backend container
289 - Uploads in progress are terminated when the limit is exceeded
290
291 **Use cases:**
292 - File upload services: increase to `1GB` or higher
293 - CI/CD artifact uploads: adjust based on your largest artifacts
294 - Tight security: reduce to `100MB` or lower
295 - Default (512MB): suitable for most applications
296
297 > **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.
298
299## Maximum Registry Blob Chunk Size
300
301The `max_blob_chunk_size` setting limits the size of a single blob upload request to the registry (`PATCH`/`PUT` under `/v2/*/blobs/uploads/*`):
302
303```toml
304[server]
305max_blob_chunk_size = "95MB"  # Default
306```
307
308**Purpose:** Prevents excessive memory usage during blob uploads while allowing large real-world image layers.
309
310**Supported units:** B, KB, MB, GB, TB (case-insensitive, decimal/1024-based)
311
312**Examples:**
313
314```toml
315max_blob_chunk_size = "256MB"  # Stricter limit
316max_blob_chunk_size = "1GB"    # Allow larger layers
317```
318
319**Behavior:**
320- When a blob chunk exceeds the limit, Gordon returns `413 Request Entity Too Large`
321- Gordon CLI uploads image layers in 50MB chunks by default, and this setting caps the maximum chunk accepted per request
322- Keep this below Cloudflare's 100MB per-request limit when operating behind Cloudflare; the default (`95MB`) is fine
323- Increase it only if you expect larger direct registry uploads from other clients
324
325## Maximum Registry Blob Size
326
327The `max_blob_size` setting limits the cumulative size of one blob/layer upload across all chunks:
328
329```toml
330[server]
331max_blob_size = "1GB"  # Default
332```
333
334**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.
335
336**Behavior:**
337- Gordon tracks the current upload size and rejects appends that would exceed the limit.
338- Rejected uploads return an OCI-compatible `413` size error.
339- Failed uploads are cleaned up so temporary blob data does not accumulate indefinitely.
340
341## Registry IP Allowlist
342
343The `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).
344
345```toml
346[server]
347registry_allowed_ips = [
348  "100.64.0.0/10",   # Tailscale CGNAT range
349  "203.0.113.50",    # single IP (treated as /32)
350]
351```
352
353**Behavior:**
354- Single IPs are automatically converted to `/32` (IPv4) or `/128` (IPv6)
355- Denied requests receive `403 Forbidden`
356- Localhost (`127.0.0.0/8`, `::1`) is always allowed so Gordon can pull from its own registry
357- Respects `trusted_proxies` for accurate client IP extraction behind reverse proxies
358- Applied before rate limiting and authentication
359- Changes require a Gordon server restart; not picked up by config hot-reload
360
361**Use cases:**
362- Restrict registry to Tailscale network: `["100.64.0.0/10"]`
363- Restrict to private subnets: `["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]`
364- Allow a specific CI server: `["100.64.0.0/10", "203.0.113.50"]`
365
366Via environment variable:
367
368```bash
369GORDON_SERVER_REGISTRY_ALLOWED_IPS=100.64.0.0/10
370```
371
372## Proxy Origin IP Allowlist
373
374The `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.
375
376```toml
377[server]
378# Only accept proxy traffic from Cloudflare edge IPs
379proxy_allowed_ips = [
380  "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
381  "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
382  "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
383  "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
384  "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
385]
386```
387
388**Behavior:**
389- Checks the direct connection IP (`RemoteAddr`), not `X-Forwarded-For`
390- Localhost is always allowed
391- Denied requests receive `403 Forbidden`
392- An empty list allows all traffic (default)
393
394**Important:** Cloudflare publishes their IP ranges at [cloudflare.com/ips](https://www.cloudflare.com/ips/). Keep your allowlist updated when Cloudflare adds new ranges.
395
396## Registry Listen Address
397
398The `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.
399
400```toml
401[server]
402registry_listen_address = "127.0.0.1"  # Loopback only
403```
404
405## HSTS
406
407HSTS headers are sent automatically on the HTTPS listener (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.
408
409## Examples
410
411### Development Configuration
412
413```toml
414[server]
415port = 8080
416registry_port = 5000
417gordon_domain = "gordon.local"
418data_dir = "./dev-data"
419```
420
421### Production Configuration
422
423```toml
424[server]
425port = 8080
426registry_port = 5000
427gordon_domain = "gordon.company.com"
428data_dir = "/var/lib/gordon"
429```
430
431### Rootless Container Setup
432
433```toml
434[server]
435port = 8080      # High port for rootless
436registry_port = 5000
437gordon_domain = "gordon.mydomain.com"
438```
439
440With firewall port forwarding:
441```bash
442sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
443sudo firewall-cmd --reload
444```
445
446## Related
447
448- [Configuration Overview](./index.md)
449- [Installation Guide](../installation.md)
450- [Routes Configuration](./routes.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