Caddy Reverse Proxy with Docker Compose and HTTPS (2026)
Caddy is a web server and reverse proxy with automatic HTTPS. A reverse proxy for one site needs two lines in the Caddyfile: the domain, and reverse_proxy app:80. Caddy gets a Let's Encrypt certificate, redirects HTTP to HTTPS and renews the certificate on its own. In Docker Compose, run the caddy:2.11 image, publish ports 80 and 443, and keep /data in a volume so certificates survive restarts.
Every file on this page was tested with Caddy 2.11 and Docker 29.
Comparing proxies first? See Caddy alternatives.
Before you start
- A Linux server with Docker and Compose. See Install Docker Compose.
- A domain with an A record that points to the server. This guide uses
example.com. - Ports 80 and 443 open in the firewall. Let's Encrypt connects to the server to check that you own the domain.
Caddy with Docker Compose
Create a folder such as /srv/proxy with this layout:
/srv/proxy/
├── compose.yaml
└── conf/
└── Caddyfile
compose.yaml:
services:
caddy:
image: caddy:2.11
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
app:
image: traefik/whoami
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
conf/Caddyfile:
{
email you@example.com
}
example.com {
reverse_proxy app:80
}
Start it:
docker compose up -d
docker compose logs -f caddy
Wait for certificate obtained successfully in the log, then open https://example.com.
app is the Compose service name. Caddy reaches it over the Docker network, so the app does not need a ports: entry. Replace traefik/whoami with your own image and 80 with the port your app listens on.
What Caddy does automatically
- Gets a certificate from Let's Encrypt for every domain in the Caddyfile. If Let's Encrypt fails, it tries ZeroSSL.
- Redirects HTTP to HTTPS (a
308redirect). - Renews certificates before they expire.
- Serves HTTP/3. That is why the compose file publishes
443/udp. - Sets
X-Forwarded-For,X-Forwarded-ProtoandX-Forwarded-Hostfor your app.
The email line is optional. Let's Encrypt uses it to warn you about problems with your certificates.
Why the volumes matter
| Volume | Holds | If you lose it |
|---|---|---|
caddy_data → /data | Certificates and the ACME account key | Caddy requests every certificate again. With many restarts you hit Let's Encrypt rate limits. |
caddy_config → /config | The last loaded config | Not critical. |
Never run Caddy without a volume on /data.
Several sites on one server
Add one block per domain:
{
email you@example.com
}
example.com {
reverse_proxy app:80
}
api.example.com {
reverse_proxy api:8000
}
www.example.com {
redir https://example.com{uri} permanent
}
Each domain gets its own certificate. Then reload Caddy (see the next section).
Apps in a different Compose project are on a different Docker network. Create a shared network with docker network create proxy, attach Caddy and the app to it with networks: [proxy], and declare it as external: true in both files. See Docker Compose networks.
Reload the Caddyfile without downtime
docker compose exec -w /etc/caddy caddy caddy validate --config Caddyfile
docker compose exec -w /etc/caddy caddy caddy reload
validate checks the file. reload applies it with no dropped connections. If the new config is invalid, Caddy keeps running the old one.
Mount the conf folder, not the single Caddyfile. Many editors save a file by replacing it. A single-file bind mount then still points to the old file, and caddy reload loads old config with no error.
Useful Caddyfile options
example.com {
encode zstd gzip
reverse_proxy app:80 {
health_uri /health
}
log {
output stdout
}
}
encodecompresses responses.health_urimakes Caddy check the app and stop sending traffic to it when it fails.- To load-balance, list several backends:
reverse_proxy app1:80 app2:80.
Test with the Let's Encrypt staging server
While you test, use Let's Encrypt staging to stay clear of rate limits. Add this to the global block at the top of the Caddyfile:
{
email you@example.com
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
Browsers do not trust staging certificates. Remove the line when everything works and reload.
Local development
For a domain that is not public, such as localhost or app.internal, Caddy creates its own local certificate authority:
localhost {
reverse_proxy app:80
}
app.internal {
tls internal
reverse_proxy app:80
}
Browsers warn about these certificates until you trust Caddy's root certificate. Inside Docker, it is at /data/caddy/pki/authorities/local/root.crt.
Troubleshooting
No certificate, certificate obtained never appears
Run docker compose logs caddy | grep -i error. Common causes:
- The DNS record does not point to this server yet. Check with
dig +short example.com. - Port 80 or 443 is blocked by a firewall or by another program on the host.
sudo ss -ltnp | grep -E ':80|:443'shows what listens there. - Too many failed attempts. Switch to staging (above), fix the problem, then switch back.
502 Bad Gateway
Caddy cannot reach the app. The service name or port is wrong, the app listens only on 127.0.0.1 inside its container (make it listen on 0.0.0.0), or the app is on a different Docker network.
ERR_TOO_MANY_REDIRECTS behind Cloudflare
Cloudflare's SSL mode is set to Flexible. Cloudflare then talks to Caddy over HTTP, and Caddy redirects to HTTPS forever. Set the SSL mode to Full (strict).
Changes in the Caddyfile do nothing
You mounted the single file, or you did not reload. Mount the folder (see above) and run caddy reload.
Install Caddy without Docker (Ubuntu / Debian)
Caddy also runs as a system service from its official apt repository:
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 config is in /etc/caddy/Caddyfile. Use reverse_proxy 127.0.0.1:3000 for an app on the same server, and apply changes with sudo systemctl reload caddy.
When to use Caddy
- You want HTTPS with no setup.
- You want a config file you can read in ten seconds.
- You run a few apps on one server.
When not to use it:
- You add and remove containers all the time and want routes from labels. Traefik does that. See Traefik with Docker Compose.
- You need advanced caching or very detailed tuning. See Nginx with Docker Compose.
- You want a web UI. See Caddy alternatives.
Frequently asked questions
How do I use Caddy as a reverse proxy?
Write the domain and a reverse_proxy line in the Caddyfile, for example 'example.com { reverse_proxy app:80 }'. Caddy gets a Let's Encrypt certificate for the domain, redirects HTTP to HTTPS and forwards requests to the app. Apply changes with caddy reload.
How do I run Caddy with Docker Compose?
Use the caddy:2.11 image, publish ports 80, 443 and 443/udp, mount a folder with your Caddyfile at /etc/caddy, and mount a named volume at /data. Put your app in the same compose file and refer to it by its service name in reverse_proxy.
Where does Caddy store certificates in Docker?
In /data inside the container, under /data/caddy/certificates. Mount a named volume there. Without it, Caddy requests new certificates after every container recreation and can hit Let's Encrypt rate limits.
How do I reload Caddy in Docker without downtime?
Run docker compose exec -w /etc/caddy caddy caddy reload. Caddy applies the new Caddyfile with no dropped connections and keeps the old config if the new one is invalid. Mount the config folder, not the single file, or the container may not see your edits.
Why does Caddy return 502 Bad Gateway?
Caddy cannot connect to the backend. Check that the service name and port in reverse_proxy are correct, that the app listens on 0.0.0.0 and not 127.0.0.1 inside its container, and that Caddy and the app share a Docker network.
Caddy or Nginx?
Caddy if you want automatic HTTPS and a short config. Nginx if you need detailed caching, rewrites and tuning, or already know its config format. Both run well in Docker Compose.
Related
- Nginx reverse proxy with Docker Compose
- Traefik with Docker Compose
- HAProxy with Docker Compose
- Caddy alternatives
Deploy without managing a proxy
Hostim.dev gives every app HTTPS, a domain, logs and metrics. No Caddyfile, no certificate volume.
👉 Deploy an app with automatic HTTPS