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