Docker Repository - bfren ecosystem
Reverse proxy built on Caddy, configured using a simple JSON file. Caddy requests and renews SSL certificates from Let's Encrypt automatically, using either the HTTP challenge or the DNS challenge (via deSEC), which also supports wildcard certificates.
This is the successor to bfren/nginx-proxy - see Migrating from nginx-proxy.
Ports 80 and 443 need mapping from the host to your proxy container, e.g. adding "0.0.0.0:80:80" to the ports section of your docker compose file. Port 80 is needed for the HTTP challenge and to redirect HTTP to HTTPS. Map 443/udp as well to enable HTTP/3.
Caddy runs as the www user (UID 1000), not root, and no extra capabilities are needed: Docker allows unprivileged users to bind to ports below 1024 inside a container by default. If you use host networking (network_mode: host) the host's setting applies instead, so you will need to set net.ipv4.ip_unprivileged_port_start=80 (or lower) on the host.
| Volume | Purpose |
|---|---|
/ssl | Your conf.json file (see proxy-conf-sample.json), basic auth users (users.json), and Caddy's certificates and ACME account data (in /ssl/caddy). |
/sites | Per-domain Caddy configuration, generated from conf.json - see Custom Domain Configuration. |
/www | /www/public is served for BF_PROXY_DOMAIN, and contains the maintenance page shown when upstream servers are unavailable. |
Files and directories in all three volumes are owned by www (UID / GID 1000), so they can be edited by the main user account on the host.
| Variable | Values | Description | Default |
|---|---|---|---|
BF_PROXY_DOMAIN | Domain name | The domain of the proxy server - requests for unknown hosts are redirected here. | None - required |
BF_PROXY_LETS_ENCRYPT_EMAIL | Email address | Used by Let's Encrypt for account registration and notification emails. | None - required |
BF_PROXY_LETS_ENCRYPT_LIVE | 0 or 1 | Only set to 1 (to request live certificates) when your config is correct - Let's Encrypt rate limits requests. | 0 |
BF_PROXY_ACME_CHALLENGE | http or dns | Default challenge type - can be overridden per domain in conf.json. | http |
BF_PROXY_DESEC_TOKEN | deSEC API token | Required to use the DNS challenge - see DNS Challenge. | None |
BF_PROXY_DNS_PROPAGATION_DELAY | Duration, e.g. 60s | Time to wait after creating the DNS record before checking for propagation. | None |
BF_PROXY_DNS_PROPAGATION_TIMEOUT | Duration, e.g. 5m | How long to wait for DNS propagation. | None |
BF_PROXY_DNS_RESOLVERS | Space-separated IPs | DNS resolvers used to check propagation, e.g. 1.1.1.1 9.9.9.9. | None |
BF_PROXY_SSL_REDIRECT_TO_CANONICAL | 0 or 1 | Default for redirectToPrimary - if 1, aliases are redirected to the primary domain. | 0 |
BF_PROXY_AUTO_RELOAD | 0 or 1 | If 1, configuration is regenerated and reloaded when conf.json, users.json or /sites change. | 0 |
BF_PROXY_UPSTREAM_RETRY | Duration, e.g. 5s | How long to keep retrying an upstream that cannot be reached (e.g. while it restarts) - 0s to disable. | 5s |
BF_PROXY_HARDEN | 0 or 1 | If 1, only TLS 1.3 will be allowed (some older devices may not be able to connect). | 0 |
BF_PROXY_BLOCK_AI_BOTS | 0 or 1 | If 1, requests from AI crawlers that collect training data receive 403 Forbidden - see below. | 1 |
BF_PROXY_ACCESS_LOG | 0 or 1 | If 1, access logs are written to the container output. | 0 |
BF_PROXY_CLEAN_INSTALL | 0 or 1 | If 1, all domain configuration and certificates are deleted on startup. | 0 |
BF_PROXY_USE_INTERNAL_CA | 0 or 1 | If 1, Caddy's internal CA issues (untrusted) certificates instead of Let's Encrypt - for testing / local use. | 0 |
BF_PROXY_AUTO_PRIMARY | Domain name | If set (with BF_PROXY_AUTO_UPSTREAM) and conf.json does not exist, it will be generated on startup. | None |
BF_PROXY_AUTO_UPSTREAM | URL | See BF_PROXY_AUTO_PRIMARY. | None |
BF_PROXY_AUTO_ALIASES | Space-separated names | Aliases to add to the auto-generated conf.json (aliases will be redirected to the primary domain). | None |
BF_PROXY_AUTO_CUSTOM | 0 or 1 | Mark the auto-generated domain as custom. | 0 |
BF_PROXY_MAINTENANCE_REFRESH_SECONDS | Integer | The number of seconds before the maintenance page auto-refreshes. | 6 |
Domains are defined in /ssl/conf.json - see proxy-conf-sample.json for an example and proxy-conf-schema.json for the full definition. Add the $schema property to get validation and autocomplete in your editor.
{
"$schema": "https://raw.githubusercontent.com/bfren/docker-caddy-proxy/main/proxy-conf-schema.json",
"domains": [
{
"primary": "example.com",
"aliases": [ "www.example.com", "ex.com" ],
"upstream": "http://example:5000",
"redirectToPrimary": true
}
]
}
| Property | Description |
|---|---|
primary | Required. The primary domain name. |
aliases | Additional host names served by this domain. Wildcards (e.g. *.example.com) require the DNS challenge. |
upstream | Required (unless custom). URL of the upstream server, e.g. http://app:5000 - https is supported. Use a list to load balance. |
retry | How long to keep retrying an unreachable upstream before showing the maintenance page, e.g. 10s (0s to disable) - defaults to BF_PROXY_UPSTREAM_RETRY. |
lb | Load balancing policy for multiple upstreams: random (default), round_robin, least_conn, first, ip_hash, uri_hash, client_ip_hash. |
challenge | http or dns - defaults to BF_PROXY_ACME_CHALLENGE. |
redirectToPrimary | Redirect aliases to the primary domain - defaults to BF_PROXY_SSL_REDIRECT_TO_CANONICAL. |
auth | Enable HTTP basic auth: true for all users, or a list of user names - see proxy-adduser. |
headers | Response headers to add or override, e.g. { "X-Frame-Options": "DENY" }. |
routes | Additional routes, evaluated in order before the default upstream - see below. |
compress | Set to false to stop responses being compressed (zstd or gzip, on by default). |
clacks | Set to false to remove the X-Clacks-Overhead: GNU Terry Pratchett response header (added by default). |
custom | See Custom Domain Configuration. |
Existing conf.json files from nginx-proxy (using primary, upstream, aliases and custom) work without changes.
Each route can have a path matcher (e.g. /api/* - if omitted the route matches everything) and must have exactly one of:
| Action | Description |
|---|---|
upstream | Proxy to this upstream (or list of upstreams, with optional lb). Add stripPrefix to remove a path prefix before proxying. |
redirect | Redirect to this URL (default status 301, set status to change). Caddy placeholders such as {http.request.uri.path} work. |
root | Serve static files from this directory (mount it into the container). |
"routes": [
{ "path": "/api/*", "upstream": [ "http://api1:8080", "http://api2:8080" ], "lb": "round_robin", "stripPrefix": "/api" },
{ "path": "/old/*", "redirect": "https://example.com/new{http.request.uri.path}" },
{ "path": "/files/*", "root": "/www/files" }
]
For every domain:
Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection), and X-Clacks-Overhead: GNU Terry Pratchett - because he was a legend.X-Real-IP is sent to the upstream, as well as Caddy's standard X-Forwarded-* headers. WebSockets work automatically.retry, so a quick restart is not noticed), or returns 502, 503 or 504, an auto-refreshing maintenance page is shown.BF_PROXY_DOMAIN.BF_PROXY_DOMAIN serves the files in /www/public, with static files (images, CSS, JavaScript, fonts etc.) cached for a year, and /favicon.ico returning 204 (No Content) unless you add one.
When BF_PROXY_BLOCK_AI_BOTS=1 (the default), requests from crawlers that collect AI training data receive 403 Forbidden. The list comes from ai.robots.txt: its robots.json is downloaded when the image is built (the release is set by the AI_ROBOTS_VERSION build argument), and only crawlers whose function is training data collection are kept - AI search crawlers, assistants and user-triggered agents (e.g. OAI-SearchBot, ChatGPT-User, PerplexityBot) are not blocked. The list is saved to /usr/share/caddy-proxy/ai-bots.txt.
Each domain's complete Caddy configuration is stored in /sites/<primary>.json:
route - the domain's route (added to the HTTPS server)errors - the domain's error route (shows the maintenance page)tls - the domain's certificate automation policy, including the ACME challengeIf the file does not exist it is generated from the standard base. What happens next depends on custom:
"custom": false (default) - the file is regenerated every time the container starts (or proxy-regenerate is run), so any changes you make to it will be lost. To add to the generated configuration, add *.json files to /sites/<primary>.d - each should contain a Caddy route object or an array of routes. They are added in file name order, after the security headers and basic auth (so they cannot bypass them) and before the routes from conf.json and the default upstream. Nginx *.conf files from nginx-proxy are ignored (a warning is logged).
Files ending .before.json (e.g. 10-health.before.json) are added first instead - before the alias redirect, AI bot blocking, security headers, compression and basic auth - so use them only for routes that must not be protected, e.g. a public /health endpoint on a site that otherwise requires a login. A warning is logged whenever they are loaded.
Within each group (*.before.json and other *.json files), files are loaded in natural sort order: numbers are compared by value (2-x.json before 10-x.json) and upper case sorts before lower case (Z.json before a.json). To keep the order predictable, use lower case names with a two-digit prefix, leaving gaps so files can be added later, e.g. 10-health.before.json, 10-api.json, 20-static.json.
"custom": true - if the file already exists it is left alone, giving you complete control of the domain's configuration, starting from the standard base. Run proxy-regenerate -d <primary> -f to regenerate it.
The DNS challenge allows certificates to be issued for wildcard domains, and for servers that are not reachable on port 80. Caddy is built with the deSEC DNS provider module.
BF_PROXY_DESEC_TOKEN to the token, and either set BF_PROXY_ACME_CHALLENGE=dns or "challenge": "dns" for each domain.BF_PROXY_DNS_PROPAGATION_DELAY (e.g. 60s) and / or BF_PROXY_DNS_RESOLVERS (e.g. 1.1.1.1).The token is not written to the generated configuration files - Caddy reads it from the environment at runtime.
By default, run proxy-regenerate after changing conf.json, users.json or files in /sites.
Set BF_PROXY_AUTO_RELOAD=1 to do this automatically: conf.json, users.json and the *.json files in /sites are checked every 5 seconds. When they change (and have stopped changing for 5 seconds, so saving several files only causes one reload) configuration is regenerated and Caddy is reloaded without dropping connections. The checks use about 1 MB of memory and very little CPU.
If the new configuration is not valid (e.g. a JSON syntax error or a missing upstream), the errors are written to the container log and Caddy keeps running with the current configuration.
| Function | Arguments | Description |
|---|---|---|
proxy-regenerate | -d: only domain, -f: force | Regenerates configuration from conf.json and reloads Caddy (with force, custom domain files are regenerated). |
proxy-reload | None | Validates the current configuration and reloads Caddy. |
proxy-validate | None | Validates conf.json and the current Caddy configuration. |
proxy-certs | None | Lists certificates with their issuer, expiry date and days left (flagged when 14 days or fewer remain). |
proxy-cleanup | -l: live mode | Removes configuration and certificates for domains not defined in conf.json (dry run unless -l is set). |
proxy-adduser | 0: username, 1: password | Adds (or updates) a user for HTTP basic auth, then regenerates configuration. |
proxy-deluser | 0: username | Removes a user, then regenerates configuration. |
/ssl/conf.json to the new /ssl volume - it is converted automatically on startup. The original is kept as /ssl/conf.json.nginx-proxy and $schema is updated. Anything that cannot be converted (e.g. custom Nginx configuration) is reported in the container log.bfren/caddy-proxy and rename environment variables from PROXY_* to BF_PROXY_* - old variable names are ignored. The following are no longer used: PROXY_ENABLE_AUTO_UPDATE (Caddy renews automatically), PROXY_ENABLE_NAXSI (NAXSI has been removed), PROXY_SSL_KEY_BITS, PROXY_SSL_DHPARAM_BITS, PROXY_GETSSL_SKIP_HTTP_TOKEN_CHECK and PROXY_UPSTREAM_DNS_RESOLVER (upstreams are resolved when requests are made)./sites cannot be converted automatically - the new per-domain files are Caddy JSON.BF_PROXY_LETS_ENCRYPT_LIVE=0 until everything works.nginx-adduser is replaced by proxy-adduser - users are stored in /ssl/users.json and enabled per domain using auth.Copyright (c) 2020-2026 bfren (unless otherwise stated)
Content type
Image
Digest
sha256:72fee4e23…
Size
52.5 MB
Last updated
about 6 hours ago
docker pull bfren/caddy-proxy