Caddyfile generator

Fill in a form, get a valid Caddyfile for reverse proxies, static sites and redirects.

Last updated

In short: To put an app behind Caddy with HTTPS, point a DNS record at your server, open ports 80 and 443, and write a site block with the domain and one line: reverse_proxy localhost:8080. Caddy fetches and renews the certificate by itself. This generator adds load balancing, health checks, basic_auth, IP allowlists, security headers and logging, and every output is tested with caddy validate.

Your input never leaves your browser. Everything is calculated on your device; nothing you type is sent to a server.

Global options
Site 1

X-Forwarded-For, -Proto and -Host are sent automatically, and WebSockets work without extra config.

Hashed in your browser at cost 14, like caddy hash-password.

    Your Caddyfile

    (security_headers) {
    	header {
    		Strict-Transport-Security "max-age=31536000; includeSubDomains"
    		X-Content-Type-Options "nosniff"
    		X-Frame-Options "SAMEORIGIN"
    		Referrer-Policy "strict-origin-when-cross-origin"
    		Permissions-Policy "camera=(), microphone=(), geolocation=()"
    		-Server
    	}
    }
    
    app.example.com {
    	import security_headers
    	encode zstd gzip
    	reverse_proxy localhost:8080
    }
    

    Check it on your server with caddy validate --config /etc/caddy/Caddyfile, then apply it with caddy reload.

    How does the Caddyfile generator work?

    A Caddyfile is Caddy's configuration file, usually /etc/caddy/Caddyfile. It is a list of site blocks: each starts with one or more addresses (a domain such as app.example.com, a wildcard, or a port such as :8080), followed by directives inside braces that say what to do with requests for that address. reverse_proxy passes them to an app, file_server serves files from disk and redir sends the visitor elsewhere. Every directive goes on its own line, with the opening brace at the end of the address line.

    HTTPS is automatic. For any public domain name, Caddy requests a certificate from Let's Encrypt or ZeroSSL, renews it, and redirects HTTP to HTTPS, as long as the domain's DNS record points at the server and ports 80 and 443 are reachable. For names only your LAN knows, tls internal uses Caddy's own certificate authority instead. When the server cannot be reached from the internet, or you want a wildcard certificate, the DNS challenge proves ownership by creating a TXT record through your DNS provider's API; that needs a Caddy build with the matching caddy-dns module.

    The generator writes the parts in the order Caddy expects: the optional global options block must come first (email, admin off), then a reusable (security_headers) snippet, then one block per site. Named matchers such as @denied not remote_ip 192.168.1.0/24 restrict a site to your own network. If Caddy runs in a podman container, put it on the same podman network as your apps and use their container names as upstreams, for example reverse_proxy jellyfin:8096.

    To apply the result, save it, run caddy validate --config /etc/caddy/Caddyfile, then caddy reload --config /etc/caddy/Caddyfile (or restart the service when the admin API is off). caddy fmt --overwrite tidies the indentation of a file you edited by hand.

    Worked examples

    One self-hosted app with HTTPS

    Input cloud.example.com → localhost:8080 gives a cloud.example.com block containing reverse_proxy localhost:8080

    The whole config is the domain, an opening brace, one reverse_proxy line and a closing brace; the directive must sit on its own line. Caddy gets a certificate on first start, redirects HTTP to HTTPS and renews it automatically well before it expires. The generator also adds encode zstd gzip and a security headers snippet, which you can untick.

    Several subdomains to several podman containers

    Input jellyfin.example.com → jellyfin:8096, photos.example.com → immich-1:2283 + immich-2:2283 gives two site blocks; the second with lb_policy least_conn and health_uri /api/server/ping

    Add one site per subdomain. When Caddy runs in a podman container on the same podman network as the apps, the container names resolve as hostnames, so the upstream is jellyfin:8096 rather than an IP. Two upstreams on one site are load balanced; the health check stops traffic to a replica that is down.

    A static website with an access log

    Input www.example.com, root /srv/www, log to /var/log/caddy/www.log gives root * /srv/www + file_server + log { output file /var/log/caddy/www.log }

    root * sets the folder for every request and file_server serves it, with index.html as the directory index. The log rolls at 10 MiB and keeps 5 old files. Add a second site with mode Redirect to send example.com to www.example.com.

    Frequently asked questions

    Does Caddy need port 80 open to get a certificate?

    Yes, for the default setup: Let's Encrypt must reach your server on port 80 (HTTP-01) or 443 (TLS-ALPN-01) from the internet. Behind a home router, forward both ports to the machine running Caddy and point the DNS A/AAAA record at your public IP. If you cannot open ports (CGNAT, or an internal-only service), use the DNS challenge, or tls internal for LAN-only names. Rootless podman cannot bind ports below 1024 by default; set net.ipv4.ip_unprivileged_port_start=80 or publish 8080/8443 and forward to those.

    How do I use the DNS challenge with Caddy and Porkbun?

    Build Caddy with the caddy-dns/porkbun module, give it an API key pair, and put a dns porkbun block inside tls. The stock image cannot do this: build one with xcaddy build --with github.com/caddy-dns/porkbun (in a Containerfile starting FROM caddy:2-builder). Keep the keys out of the Caddyfile: store them as podman secrets and expose them as environment variables (Secret=porkbun_key,type=env,target=PORKBUN_API_KEY in a quadlet), which the generated {env.PORKBUN_API_KEY} placeholders read. The DNS challenge is also the only way to get a wildcard certificate such as *.example.com.

    What is the difference between handle_path and handle in Caddy?

    handle_path strips the matched prefix before passing the request on; handle keeps the path unchanged. With handle_path /app/* a request for /app/login reaches the upstream as /login. Note that /app/* does not match /app without a trailing slash, so add redir /app /app/ above it. Many apps break under a sub-path anyway; a subdomain per app (what this generator produces) avoids the problem.

    Does Caddy reverse_proxy support WebSockets?

    Yes, with no extra configuration: reverse_proxy passes the Upgrade and Connection headers and keeps the connection open. By default Caddy closes WebSocket connections when you reload the config; set stream_close_delay (for example 5m) inside reverse_proxy if clients should survive reloads. See 'reverse_proxy' in the Caddy documentation.

    How do I generate a password hash for Caddy basic_auth?

    Run caddy hash-password and paste its output after the username in the basic_auth block. Without a local Caddy: podman run --rm -it docker.io/library/caddy:2 caddy hash-password, which prompts for the password so it does not land in your shell history. This page makes the same kind of bcrypt hash (cost 14) in your browser with bcryptjs; Caddy accepts its $2b$ hashes, which we test against Caddy 2.11. Never put the plain password in the Caddyfile.

    Does Caddy set X-Forwarded-For automatically?

    Yes. reverse_proxy sends X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host by default, so adding header_up lines for them only produces a warning. Some apps want X-Real-IP instead; tick the option to add header_up X-Real-IP {remote_host}. If another proxy or a CDN sits in front of Caddy, configure trusted_proxies so the client IP is taken from its headers.