Why choose Caddy as a reverse proxy on your VPS?
Most reverse proxies delegate TLS management to an external tool — Certbot, acme.sh or a homemade cron script. Caddy integrates this mechanism end-to-end: as soon as a domain name is declared in the Caddyfile, it contacts Let's Encrypt, obtains the certificate and renews it before expiry, without manual intervention. The result is a configuration that reads in a few lines and deploys identically across all your VPS instances, with no external state to synchronise.
What Caddy brings to production
- Automatic HTTPS: Let's Encrypt ACME HTTP-01 activated as soon as a domain name is declared — no cron, no plugin to install
- Concise syntax: a five-line block replaces a forty-line Nginx vhost with its Certbot configuration file
- Zero cold reload:
caddy reloadapplies the newCaddyfilewithout dropping in-flight connections - HTTP/2 and HTTP/3 (QUIC) by default: enabled without additional configuration on all 2.x versions
- JSON API: configuration can be updated live via a REST API, useful for dynamic or containerised environments
- Lightweight official Docker image:
caddy:2-alpineweighs under 20 MB and covers most production cases
Prerequisites
Before you start, verify that your VPS meets the following requirements.
Minimum resources: 512 MB of RAM and 1 vCPU are sufficient for Caddy alone. Allow 1 GB RAM and 2 vCPU if you host several applications behind it.
Ports 80 and 443 free: Caddy occupies them for ACME HTTP-01 (port 80) and for HTTPS traffic (port 443). Check that no other process is using them:
ss -tlnp | grep -E ':80|:443'Domain name pointing to the VPS: Let's Encrypt validates domain ownership by querying port 80 on your IP. Your DNS A record must be active and propagated before starting Caddy.
Operating system: Debian 11/12 or Ubuntu 22.04/24.04. Commands in this guide are tested on these distributions.
Docker (optional): if you deploy Caddy in a container, Docker Engine 24+ and Docker Compose v2 are required.
Deploying Caddy as a reverse proxy
Method 1 — Installation via the official APT repository
Caddy provides a signed APT repository. Add it, then install the package:
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ | tee /etc/apt/sources.list.d/caddy-stable.list apt update && apt install caddyVerify the installed version:
caddy versionshould displayv2.9.xor higher.Method 2 — Deployment with Docker Compose
If your applications already run under Docker, a Caddy service in the same
compose.ymlis the simplest approach. Create acompose.ymlfile at the root of your project:services: caddy: image: caddy:2-alpine restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config networks: - proxy volumes: caddy_data: caddy_config: networks: proxy: external: trueCreate the shared network once:
docker network create proxy. Your other services join this network to be accessible by Caddy.Create the basic Caddyfile
The
Caddyfileis the central configuration file. For an APT installation, it lives at/etc/caddy/Caddyfile. For Docker, place it next to yourcompose.yml.Here is a minimal example that exposes an application on port 3000:
myapp.com { reverse_proxy localhost:3000 }That's it. Caddy detects that
myapp.comis a domain name, contacts Let's Encrypt via ACME HTTP-01, obtains a certificate and automatically redirects all HTTP traffic to HTTPS. Certificates are stored in~/.local/share/caddy/(APT) or in thecaddy_datavolume (Docker).Multi-application configuration (multi-vhost)
Hosting multiple applications on the same VPS only requires additional blocks in the same file:
app1.yourdomain.com { reverse_proxy localhost:3000 } app2.yourdomain.com { reverse_proxy localhost:4000 } blog.yourdomain.com { reverse_proxy localhost:8080 }Each block gets its own Let's Encrypt certificate. Caddy manages renewals independently for each domain, in parallel, without service interruption.
Apply the Caddyfile without interruption
For APT, reload the configuration without dropping connections:
systemctl reload caddy # or, if you modified the file outside /etc/caddy/: caddy reload --config /path/to/CaddyfileFor Docker:
docker compose exec caddy caddy reload --config /etc/caddy/CaddyfileVerify that the syntax is correct before reloading:
caddy validate --config /etc/caddy/Caddyfile.Add security headers and rate limiting
Caddy supports native security headers via the
headerdirective. Here is a hardened block for a production application:myapp.com { reverse_proxy localhost:3000 header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "SAMEORIGIN" Referrer-Policy "strict-origin-when-cross-origin" -Server } log { output file /var/log/caddy/myapp.log { roll_size 10mb roll_keep 5 } } }For rate limiting, the
caddy-ratelimitmodule is installed viaxcaddy buildand used with therate_limitdirective. The official Docker image does not include it — build your own image or use an upstream application firewall if the need is critical.Enable and verify the systemd service (APT)
After installation via APT, the service is enabled automatically. Check its status and logs:
systemctl status caddy journalctl -u caddy -fTo ensure Caddy starts at reboot:
systemctl enable caddy(already done by the package). Test that the Let's Encrypt certificate was successfully issued:curl -sv https://myapp.com 2>&1 | grep -E 'subject|issuer|expire'
How Caddy's automatic HTTPS works
Caddy implements the ACME (Automated Certificate Management Environment) protocol natively. As soon as a Caddyfile block contains a qualified domain name, Caddy automatically triggers the ACME HTTP-01 challenge: Let's Encrypt places a verification token at http://yourdomain.com/.well-known/acme-challenge/, Caddy responds to this request and proves it controls the server. Let's Encrypt then issues a certificate valid for 90 days.
Caddy renews this certificate in advance (starting 30 days before expiry) and reloads the configuration hot, without service interruption. Certificates and ACME data are stored locally — in ~/.local/share/caddy/ for an APT installation, in the caddy_data volume for Docker. Do not delete this volume: Caddy also stores ACME state there, and too many requests to Let's Encrypt trigger rate limits that can block new certificate issuance for several hours.
For wildcard certificates (*.yourdomain.com), ACME HTTP-01 does not work: the DNS-01 challenge is required, which needs access to your DNS provider's API. Caddy natively supports several providers via modules (caddy-dns/cloudflare, caddy-dns/ovh…) compiled with xcaddy.
Caddy vs Nginx: when to choose which
Scroll the table
| Criterion | Caddy | Nginx |
|---|---|---|
| TLS configuration | Automatic, zero manual configuration | Manual (Certbot or acme.sh required) |
| Learning curve | Low — readable syntax, few directives | Moderate — verbose syntax, many contexts |
| Raw performance (req/s) | Excellent for most workloads | Slightly higher under very high load |
| Modules and ecosystem | Growing, xcaddy to compile modules | Very large, numerous mature third-party modules |
| Reload without interruption | Native (`caddy reload`) | Native (`nginx -s reload`), similar |
| Ideal use case | Personal VPS, team projects, Docker microservices | Large platforms, CDN frontend, advanced cases (GeoIP, Lua…) |
| HTTP/3 (QUIC) support | Enabled by default since Caddy 2.6 | Experimental, requires compilation with quiche or ngx_http_v3 |
Troubleshooting: common errors
Port 80 already in use. Caddy needs port 80 for ACME HTTP-01. If Apache or Nginx is already running, stop it before starting Caddy: systemctl stop apache2 or systemctl stop nginx. Then verify with ss -tlnp | grep :80.
ACME failure / certificate not issued. Let's Encrypt cannot reach your server. Check: (1) your DNS A record points to the VPS IP — dig +short your-domain.com must return your IP; (2) port 80 is accessible from the outside — VPS firewalls (iptables, ufw) sometimes block this port even when Caddy is listening. Check Caddy logs with journalctl -u caddy to read the precise ACME error message.
permission denied on ports 80/443. Ports below 1024 are reserved under Linux. For an APT installation, the package automatically configures the CAP_NET_BIND_SERVICE capability on the Caddy binary. If you compiled Caddy manually, apply it: setcap cap_net_bind_service=+ep $(which caddy).
DNS not resolved / no such host. Caddy resolves the names of backends declared in reverse_proxy. If your application is called app in Docker Compose, make sure Caddy and the application share the same Docker network. Verify with: docker compose exec caddy nslookup app.
caddy_data volume accidentally deleted. Caddy must renegotiate all certificates from scratch. Let's Encrypt enforces a limit of 5 certificates per domain over 7 days. If you hit this limit, use the ACME staging environment (acme_ca https://acme-staging-v02.api.letsencrypt.org/directory in the Caddyfile) to test, then switch back to production once the window has passed.
For hosting multiple applications on the same VPS
Create a shared Docker network (docker network create proxy) and connect each service to that network. The Caddyfile then references services by their container name rather than localhost:PORT — the configuration does not change when you add or remove applications. Example: reverse_proxy my-service:3000 instead of reverse_proxy localhost:3000.
Official documentation
The complete reference for directives, modules and the JSON API is available at <a href="https://caddyserver.com/docs/">caddyserver.com/docs</a>. The community forum (<a href="https://caddy.community">caddy.community</a>) is the go-to community reference for advanced questions — the maintainers reply directly.