Caddy Reverse Proxy: Automatic HTTPS for Network Services - 夜莺博客

Caddy Reverse Proxy: Automatic HTTPS for Network Services

Caddy is the shortest path from “I have a service on port 9000” to “I have a TLS-terminated hostname with a renewable certificate.” Where nginx needs a certificate workflow bolted on and Traefik needs a provider configured, Caddy treats HTTPS as the default behaviour and only asks that you tell it which hostname you are serving. This guide covers the one-line start, the Caddyfile you will actually run in a lab, and the two failure modes that account for most Caddy tickets.

One Command to a Working Proxy

caddy reverse-proxy --from example.com --to localhost:9000

If you omit --from, Caddy defaults to localhost and issues a locally trusted certificate from its own CA (you may be prompted once to install the root into your trust store). With a real hostname, Caddy performs the ACME HTTP-01 or TLS-ALPN challenge and provisions a publicly trusted certificate without any further configuration.

Binding to ports 80 and 443 requires privilege. Two options:

sudo -E caddy reverse-proxy --from example.com --to localhost:9000
# or, no root needed
sudo setcap cap_net_bind_service=+ep $(which caddy)

The Caddyfile You Will Actually Keep

app.example.com {
    encode gzip zstd
    reverse_proxy 10.10.10.50:8080 {
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}
        health_uri /healthz
        health_interval 10s
        lb_policy least_conn
    }
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options nosniff
        X-Frame-Options SAMEORIGIN
    }
    log {
        output file /var/log/caddy/app.log {
            roll_size 50MiB
            roll_keep 5
        }
    }
}

Caddy reloads without dropping connections:

caddy validate --config /etc/caddy/Caddyfile
caddy reload --config /etc/caddy/Caddyfile

HTTPS to the Backend

app.example.com {
    reverse_proxy https://10.10.10.51:8443 {
        transport http {
            tls_trust_pool file /etc/ssl/internal-ca.pem
            tls_server_name app.internal
        }
    }
}

Trust the upstream CA explicitly rather than reaching for tls_insecure_skip_verify. Since Caddy v2.11 the Host header is set to match the upstream automatically, so proxying to an HTTPS backend that routes on SNI no longer needs a manual header_up Host {upstream_hostport}.

When Automatic HTTPS Does Not Activate

  • The site address starts with http:// — that explicitly disables automation for that site.
  • You configured only a port and no hostname, so Caddy has nothing to get a certificate for.
  • You load certificates manually without setting ignore_loaded_certificates.
  • Automatic HTTPS is disabled globally in the global options block.

The Two Most Common Failures

1. Certificate issuance fails because port 80 is blocked

The HTTP-01 challenge needs port 80 reachable from the internet. The TLS-ALPN challenge needs 443. If neither is possible, switch to the DNS challenge, which needs provider credentials instead of open ports:

{
    acme_dns cloudflare {env.CF_API_TOKEN}
}

2. The backend sees the wrong client IP

Applications behind Caddy must be told which socket to trust. Set X-Forwarded-For and X-Real-IP at the proxy and configure the application to read them from a trusted proxy range only — otherwise an attacker can spoof the header and bypass IP-based access controls.

Reaching Non-HTTP Services

jellyfin.example.com {
    reverse_proxy 10.10.10.60:8096
}
# On-demand certificates for a dynamic set of hostnames
{
    on_demand_tls {
        ask http://127.0.0.1:8080/check-domain
    }
}

On-demand TLS issues certificates at handshake time for hostnames not in the config — powerful for multi-tenant setups, and dangerous without an ask endpoint that gates which domains Caddy may obtain certificates for.

Related Reading

Deeper dives on the same topics from our archive:

原文链接:https://caddyserver.com/docs/quick-starts/reverse-proxy