Operations

Reverse Proxy With Automatic HTTPS Using Caddy

Running several web applications on one VPS usually means juggling ports, TLS certificates, and renewal cron jobs. Caddy collapses most of that work into a single readable config file. It is a web server written in Go that terminates TLS, reverse-proxies to your backends, and provisions Let's Encrypt certificates automatically, with no separate ACME client to install or babysit. This guide covers a current (2026) Caddy v2 install on a Debian or Ubuntu VPS, a clean Caddyfile that proxies to app backends, compression and header hardening, and how the model differs from Nginx.

What Caddy needs before you start

Caddy's automatic HTTPS relies on the ACME protocol, so two prerequisites are non-negotiable:

  • Ports 80 and 443 reachable from the public internet. Caddy uses port 80 for the HTTP-01 challenge and the HTTP to HTTPS redirect, and port 443 for TLS and the TLS-ALPN challenge. If a firewall or security group blocks either, certificate issuance fails.
  • A real domain name resolving to your server. ACME certificate authorities validate control of a public hostname, so localhost or a bare IP will not get a publicly trusted certificate.

You will need a domain with an A record pointing at your VPS IPv4 address (and an AAAA record if you serve IPv6) before Caddy can request certificates.

Installing Caddy on Debian or Ubuntu

Caddy publishes an official APT repository, which is the method the project recommends because it keeps the binary updated through your normal package workflow. Run the following as a user with sudo:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

The package installs the binary at /usr/bin/caddy, creates a caddy system user, drops a default config at /etc/caddy/Caddyfile, and enables a caddy.service systemd unit. Confirm the install and check which version you are running:

caddy version
systemctl status caddy

On Fedora or RHEL-family systems the equivalent is a COPR repository (dnf copr enable @caddy/caddy then dnf install caddy), and static binaries are available from the project's GitHub releases. See the official install docs for every supported method.

A minimal reverse proxy Caddyfile

Edit /etc/caddy/Caddyfile. The smallest useful config that gives you automatic HTTPS in front of an app listening on a local port is two lines:

app.example.com {
    reverse_proxy 127.0.0.1:8080
}

That is the whole thing. Because you named a real domain, Caddy will request a certificate on first start, redirect HTTP to HTTPS, terminate TLS, and forward decrypted requests to your backend on port 8080. Apply changes without a full restart by reloading:

sudo systemctl reload caddy

Reload validates the new config and swaps it in gracefully, so a syntax error does not take your running sites down. You can also validate a file before reloading with caddy validate --config /etc/caddy/Caddyfile.

Hosting multiple sites in one file

Each top-level block is an independent site address, so a single Caddyfile can front an entire fleet of apps. Caddy requests and renews a certificate per hostname:

api.example.com {
    reverse_proxy 127.0.0.1:8080
}

app.example.com {
    reverse_proxy 127.0.0.1:3000
}

grafana.example.com {
    reverse_proxy 127.0.0.1:3001
}

You can also point one site at several backend instances and let Caddy load balance across them. Active health checks pull unhealthy nodes out of rotation:

app.example.com {
    reverse_proxy 127.0.0.1:3000 127.0.0.1:3001 127.0.0.1:3002 {
        lb_policy round_robin
        health_uri /healthz
        health_interval 10s
    }
}

Available lb_policy values include round_robin, least_conn, ip_hash, and random (the default). For path-based routing to different services under one hostname, use a handle block with a matcher:

example.com {
    handle /api/* {
        reverse_proxy 127.0.0.1:8080
    }
    handle {
        reverse_proxy 127.0.0.1:3000
    }
}

Compression and header hygiene

The encode directive turns on response compression. If you list no formats, Caddy enables zstd (preferred) and gzip by default and negotiates per request based on the client's Accept-Encoding header. Being explicit is fine too:

app.example.com {
    encode zstd gzip

    reverse_proxy 127.0.0.1:8080 {
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

Only responses at or above a minimum size are compressed (the default threshold is 512 bytes), which avoids wasting cycles on tiny payloads. The header_up lines add request headers sent to the backend so your application can see the client's real address and scheme. Note that Caddy already sets X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host by default, so add these only if your app expects specific field names.

To set response headers for the browser, use the top-level header directive. A conservative baseline:

app.example.com {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }
    encode zstd gzip
    reverse_proxy 127.0.0.1:8080
}

The leading - on -Server removes that response header. The same minus syntax works with header_up and header_down inside reverse_proxy to strip or rewrite proxied headers. Keep Strict-Transport-Security off until you are certain every subdomain will stay on HTTPS, since it is sticky in browsers.

How this compares to Nginx

Nginx is fast, battle-tested, and ubiquitous, but TLS is a separate concern: you install Certbot, wire up a renewal timer, and manage certificate paths in each server block. Caddy folds certificate issuance and renewal into the server itself, which removes a whole category of expired-certificate incidents.

  • TLS: Caddy is automatic and on by default. Nginx needs Certbot or manual certificates.
  • Config surface: a working Caddy proxy is a few lines. The Nginx equivalent with TLS, redirects, and proxy headers is considerably longer.
  • Performance ceiling: Nginx still has an edge in raw throughput and mature tuning knobs for very high traffic and complex caching. For most application backends the difference is not the bottleneck.
  • Ecosystem: Nginx has a larger body of third-party modules and community recipes. Caddy's plugin model is clean but smaller.

A reasonable rule of thumb: reach for Caddy when you want correct HTTPS with minimal config and fast iteration, and stay on Nginx when you have existing tuned configs or need its specific caching and module ecosystem.

Wrapping up

Caddy gives you production-grade TLS termination and reverse proxying with a config file short enough to read at a glance. Point a domain at your VPS, open ports 80 and 443, install from the official repository, and describe your sites in a Caddyfile. Add encode for compression and a small header block for security defaults, and you have a maintainable front door for one app or a dozen. For the full directive reference, the Caddyfile documentation is the authoritative source.