What is Caddy?
Caddy is a modern web server and reverse proxy written in Go, best known for automatic HTTPS: give it a domain name and it obtains and renews a Let's Encrypt or ZeroSSL certificate with no extra steps. It supports HTTP/2 and HTTP/3 out of the box, has a famously short config format (the Caddyfile), and is open source under the Apache-2.0 license. In a self-hosted setup it is the front door that serves all your apps over HTTPS.
Requirements
- A Linux server with Docker Engine and Docker Compose v2. Caddy is tiny: 256 MB of RAM is plenty.
- A domain with A records (or a wildcard) for each hostname you want to serve.
- Ports 80 and 443 open to the internet, and no other web server already using them.
Step 1: Prepare the server
This guide assumes Ubuntu 24.04 with Docker installed from the official Docker Engine guide.
mkdir -p ~/caddy/conf ~/caddy/site && cd ~/caddy
docker network create proxy
The shared proxy network lets Caddy reach other Compose projects by container name, so those apps never need published ports.
Step 2: Create the Docker Compose file
This follows the Compose example from the official Caddy image docs. Save it as docker-compose.yml:
services:
caddy:
image: caddy:2.11
container_name: caddy
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./site:/srv
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
volumes:
caddy_data:
caddy_config:
networks:
proxy:
external: true
443/udp enables HTTP/3, and NET_ADMIN lets the QUIC library tune UDP buffer sizes. The 2.11 tag follows Caddy 2.11 patch releases.
Now create conf/Caddyfile:
{
email you@example.com
}
example.com {
root * /srv
file_server
}
app.example.com {
reverse_proxy whoami:80
}
The first block sets the contact email for certificate expiry notices. The second serves static files from ./site. The third proxies to a container named whoami on the proxy network.
Step 3: Start and open the app
Start a test container on the shared network, then Caddy:
docker run -d --name whoami --network proxy --restart unless-stopped traefik/whoami:v1.12
echo '<h1>It works</h1>' > site/index.html
docker compose up -d
docker compose logs -f caddy
Within a few seconds the logs show "certificate obtained successfully" for each hostname. Open https://example.com for the static page and https://app.example.com to see the whoami response headers. HTTP requests redirect to HTTPS automatically.
Step 4: Put it behind HTTPS
Caddy is the HTTPS layer, so this step is about adding your real apps. In each app's Compose file, join the external proxy network and remove its ports: section:
services:
myapp:
image: your/app
networks: [proxy]
networks:
proxy:
external: true
Add a site block such as myapp.example.com { reverse_proxy myapp:3000 } and reload. Removing published ports matters: Docker-published ports bypass ufw, so an app published on 0.0.0.0 stays reachable over plain HTTP no matter what the firewall says. If an app must stay on the host network, publish it on 127.0.0.1 and proxy to host.docker.internal after adding extra_hosts: ["host.docker.internal:host-gateway"] to Caddy.
Backups and upgrades
Back up the conf folder and the caddy_data volume (certificates and keys):
docker run --rm -v caddy_caddy_data:/data -v "$PWD":/backup alpine \
tar czf /backup/caddy-data-$(date +%F).tgz -C /data .
Apply Caddyfile changes without downtime:
docker compose exec -w /etc/caddy caddy caddy reload
To upgrade, run docker compose pull && docker compose up -d. For a new minor version, change the tag and skim the release notes.
Troubleshooting
- Certificate errors in the logs: DNS does not point at this server yet, or ports 80 and 443 are blocked by a cloud firewall. Fix it and Caddy retries automatically.
- 502 Bad Gateway: Caddy cannot reach the upstream. Check the container name, port and that both are on the
proxynetwork. - "address already in use" on port 80: Apache or Nginx is running on the host. Stop and disable it.
- Too many certificates already issued: you hit Let's Encrypt rate limits by recreating Caddy without its
/datavolume. Keep the volume and wait for the limit to reset.
Next steps
Add basic_auth or forward authentication through Authentik for private apps, use encode zstd gzip for compression, set security headers with the header directive, and build a custom image with xcaddy if you need DNS-challenge plugins for wildcard certificates.
Spotted something out of date? Tell us and we will update the guide.