Sign inSign up

bfren/caddy-proxy

By bfren

•Updated about 1 hour ago

Image
0

3.4K

bfren/caddy-proxy repository overview

⁠Docker Caddy Proxy

GitHub release (latest by date) Docker Pulls Docker Image Size GitHub Workflow Status

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

⁠Contents

⁠Ports

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.

  • 80
  • 443
  • 443/udp

⁠Volumes

VolumePurpose
/sslYour conf.json file (see proxy-conf-sample.json), basic auth users (users.json), and Caddy's certificates and ACME account data (in /ssl/caddy).
/sitesPer-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.

⁠Environment Variables

VariableValuesDescriptionDefault
BF_PROXY_DOMAINDomain nameThe domain of the proxy server - requests for unknown hosts are redirected here.None - required
BF_PROXY_LETS_ENCRYPT_EMAILEmail addressUsed by Let's Encrypt for account registration and notification emails.None - required
BF_PROXY_LETS_ENCRYPT_LIVE0 or 1Only set to 1 (to request live certificates) when your config is correct - Let's Encrypt rate limits requests.0
BF_PROXY_ACME_CHALLENGEhttp or dnsDefault challenge type - can be overridden per domain in conf.json.http
BF_PROXY_DESEC_TOKENdeSEC API tokenRequired to use the DNS challenge - see DNS Challenge⁠.None
BF_PROXY_DNS_PROPAGATION_DELAYDuration, e.g. 60sTime to wait after creating the DNS record before checking for propagation.None
BF_PROXY_DNS_PROPAGATION_TIMEOUTDuration, e.g. 5mHow long to wait for DNS propagation.None
BF_PROXY_DNS_RESOLVERSSpace-separated IPsDNS resolvers used to check propagation, e.g. 1.1.1.1 9.9.9.9.None
BF_PROXY_SSL_REDIRECT_TO_CANONICAL0 or 1Default for redirectToPrimary - if 1, aliases are redirected to the primary domain.0
BF_PROXY_AUTO_RELOAD0 or 1If 1, configuration is regenerated and reloaded when conf.json, users.json or /sites change.0
BF_PROXY_UPSTREAM_RETRYDuration, e.g. 5sHow long to keep retrying an upstream that cannot be reached (e.g. while it restarts) - 0s to disable.5s
BF_PROXY_HARDEN0 or 1If 1, only TLS 1.3 will be allowed (some older devices may not be able to connect).0
BF_PROXY_BLOCK_AI_BOTS0 or 1If 1, requests from AI crawlers that collect training data receive 403 Forbidden - see below.1
BF_PROXY_ACCESS_LOG0 or 1If 1, access logs are written to the container output.0
BF_PROXY_CLEAN_INSTALL0 or 1If 1, all domain configuration and certificates are deleted on startup.0
BF_PROXY_USE_INTERNAL_CA0 or 1If 1, Caddy's internal CA issues (untrusted) certificates instead of Let's Encrypt - for testing / local use.0
BF_PROXY_AUTO_PRIMARYDomain nameIf set (with BF_PROXY_AUTO_UPSTREAM) and conf.json does not exist, it will be generated on startup.None
BF_PROXY_AUTO_UPSTREAMURLSee BF_PROXY_AUTO_PRIMARY.None
BF_PROXY_AUTO_ALIASESSpace-separated namesAliases to add to the auto-generated conf.json (aliases will be redirected to the primary domain).None
BF_PROXY_AUTO_CUSTOM0 or 1Mark the auto-generated domain as custom.0
BF_PROXY_MAINTENANCE_REFRESH_SECONDSIntegerThe number of seconds before the maintenance page auto-refreshes.6

⁠Configuration

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
        }
    ]
}
PropertyDescription
primaryRequired. The primary domain name.
aliasesAdditional host names served by this domain. Wildcards (e.g. *.example.com) require the DNS challenge.
upstreamRequired (unless custom). URL of the upstream server, e.g. http://app:5000 - https is supported. Use a list to load balance.
retryHow long to keep retrying an unreachable upstream before showing the maintenance page, e.g. 10s (0s to disable) - defaults to BF_PROXY_UPSTREAM_RETRY.
lbLoad balancing policy for multiple upstreams: random (default), round_robin, least_conn, first, ip_hash, uri_hash, client_ip_hash.
challengehttp or dns - defaults to BF_PROXY_ACME_CHALLENGE.
redirectToPrimaryRedirect aliases to the primary domain - defaults to BF_PROXY_SSL_REDIRECT_TO_CANONICAL.
authEnable HTTP basic auth: true for all users, or a list of user names - see proxy-adduser.
headersResponse headers to add or override, e.g. { "X-Frame-Options": "DENY" }.
routesAdditional routes, evaluated in order before the default upstream - see below.
compressSet to false to stop responses being compressed (zstd or gzip, on by default).
clacksSet to false to remove the X-Clacks-Overhead: GNU Terry Pratchett response header (added by default).
customSee Custom Domain Configuration⁠.

Existing conf.json files from nginx-proxy (using primary, upstream, aliases and custom) work without changes.

⁠Routes

Each route can have a path matcher (e.g. /api/* - if omitted the route matches everything) and must have exactly one of:

ActionDescription
upstreamProxy to this upstream (or list of upstreams, with optional lb). Add stripPrefix to remove a path prefix before proxying.
redirectRedirect to this URL (default status 301, set status to change). Caddy placeholders such as {http.request.uri.path} work.
rootServe 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" }
]
⁠Defaults

For every domain:

  • HTTP requests are redirected to HTTPS, and certificates are requested and renewed automatically.
  • Responses are compressed using zstd or gzip, depending on what the browser supports. Images and other already-compressed formats, responses under 512 bytes, and responses the upstream has already compressed are left alone.
  • Secure headers are added (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.
  • If the upstream cannot be reached (after retrying for retry, so a quick restart is not noticed), or returns 502, 503 or 504, an auto-refreshing maintenance page is shown.
  • Requests for unknown hosts are redirected to 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.

⁠AI Crawlers

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.

⁠Custom Domain Configuration

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 challenge

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

⁠DNS Challenge

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.

  1. Create a token in your deSEC account with permission to manage the relevant domains.
  2. Set BF_PROXY_DESEC_TOKEN to the token, and either set BF_PROXY_ACME_CHALLENGE=dns or "challenge": "dns" for each domain.
  3. If certificate requests fail because the record has not propagated, set 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.

⁠Automatic Reload

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.

⁠Helper Functions

FunctionArgumentsDescription
proxy-regenerate-d: only domain, -f: forceRegenerates configuration from conf.json and reloads Caddy (with force, custom domain files are regenerated).
proxy-reloadNoneValidates the current configuration and reloads Caddy.
proxy-validateNoneValidates conf.json and the current Caddy configuration.
proxy-certsNoneLists certificates with their issuer, expiry date and days left (flagged when 14 days or fewer remain).
proxy-cleanup-l: live modeRemoves configuration and certificates for domains not defined in conf.json (dry run unless -l is set).
proxy-adduser0: username, 1: passwordAdds (or updates) a user for HTTP basic auth, then regenerates configuration.
proxy-deluser0: usernameRemoves a user, then regenerates configuration.

⁠Migrating from nginx-proxy

  1. Copy /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.
  2. In your docker compose file, change the image to 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).
  3. Custom Nginx configuration in /sites cannot be converted automatically - the new per-domain files are Caddy JSON.
  4. Existing certificates are not reused - Caddy will request new ones. Caddy requests one certificate per host name (getssl used one certificate per primary domain), so check Let's Encrypt rate limits⁠ if you have many domains, and leave BF_PROXY_LETS_ENCRYPT_LIVE=0 until everything works.
  5. nginx-adduser is replaced by proxy-adduser - users are stored in /ssl/users.json and enabled per domain using auth.

⁠Licence

Copyright (c) 2020-2026 bfren⁠ (unless otherwise stated)

Tag summary

Content type

Image

Digest

sha256:72fee4e23…

Size

52.5 MB

Last updated

about 5 hours ago

docker pull bfren/caddy-proxy