Caddy’s automatic HTTPS is so good, it can make you forget you’re even dealing with TLS.

Let’s see Caddy in action as a reverse proxy. Imagine we have a simple web app running on localhost:8080 and we want to expose it at myapp.local with Caddy handling TLS.

First, Caddy needs a configuration file, typically Caddyfile.

myapp.local {
    reverse_proxy localhost:8080
}

That’s it. No complex certificate management, no manual renewals. Caddy will automatically obtain and renew Let’s Encrypt certificates for myapp.local and serve traffic over HTTPS.

When a browser requests https://myapp.local, Caddy intercepts the request. It checks if it has a valid certificate for myapp.local. If not, it initiates the ACME (Automated Certificate Management Environment) protocol with Let’s Encrypt, typically via the HTTP-01 challenge, proving it controls the domain. Once the certificate is obtained, Caddy presents it to the browser, establishing a TLS connection. The browser then sends the original request (now encrypted) to Caddy. Caddy, acting as a reverse proxy, decrypts the request, forwards it to the backend application at localhost:8080, receives the response, encrypts it, and sends it back to the browser.

The core problem Caddy solves here is the operational burden of TLS and reverse proxying. Traditionally, this involved setting up a web server like Nginx or Apache, manually obtaining certificates from a CA, configuring TLS settings, and then setting up proxy rules. Caddy automates all of this. The Caddyfile is Caddy’s primary configuration interface. Directives like reverse_proxy tell Caddy how to handle incoming requests, routing them to upstream services. The domain name itself (myapp.local in this case) is the trigger for automatic certificate acquisition.

The most surprising thing is how seamlessly Caddy integrates with system services. You can run Caddy as a systemd service, ensuring it starts on boot and manages its own process.

sudo systemctl enable caddy
sudo systemctl start caddy

This configuration leverages Caddy’s ability to bind to privileged ports (80 and 443) and its background process management. The Caddyfile is typically placed at /etc/caddy/Caddyfile. When Caddy starts, it reads this file and applies the configured directives. For reverse proxying, the reverse_proxy directive is key. It takes one or more upstream addresses. Caddy load-balances across these addresses if multiple are provided.

The tls internal directive is an important one to understand if you’re using Caddy to serve internal-only services. By default, Caddy tries to get public Let’s Encrypt certificates. For internal domains that aren’t publicly resolvable, or if you want to avoid public CA lookups, tls internal tells Caddy to use its own internal CA to issue certificates. These certificates are trusted by Caddy itself but will cause trust warnings in browsers unless you manually import Caddy’s root CA certificate into your system’s trust store.

internalapp.local {
    tls internal
    reverse_proxy localhost:9000
}

To trust Caddy’s internal CA, you’d typically run caddy trust (as root). This command installs Caddy’s root certificate into the operating system’s trust store, allowing clients to trust certificates issued by it without warnings.

The real power comes when you start chaining directives. For example, you can add logging, set headers, or even implement authentication directly within the Caddyfile.

myapp.local {
    log {
        output stdout
        format json
    }
    header Strict-Transport-Security "max-age=31536000;"
    reverse_proxy localhost:8080
}

This configuration adds JSON-formatted logging to standard output and sets the Strict-Transport-Security header, ensuring future connections to myapp.local will be attempted over HTTPS.

The next concept you’ll likely encounter is how to manage more complex routing scenarios, such as routing based on request paths or headers, and integrating with load balancers.

Want structured learning?

Take the full Caddy course →