Skip to main content

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 308 redirect).
  • Renews certificates before they expire.
  • Serves HTTP/3. That is why the compose file publishes 443/udp.
  • Sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host for your app.

The email line is optional. Let's Encrypt uses it to warn you about problems with your certificates.

Why the volumes matter​

VolumeHoldsIf you lose it
caddy_data → /dataCertificates and the ACME account keyCaddy requests every certificate again. With many restarts you hit Let's Encrypt rate limits.
caddy_config → /configThe last loaded configNot 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
}
}
  • encode compresses responses.
  • health_uri makes 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:

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.



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