Why I Started Using Caddy
When running services from a home server or a small cloud setup, configuring HTTPS can quickly become complicated. Certificates, renewal scripts, reverse proxy configuration, and additional tooling such as certbot can add unnecessary operational overhead.
Caddy simplifies this entire process. It automatically obtains and renews Let’s Encrypt certificates and works as a reverse proxy with minimal configuration. If you’ve set up TLS the older way with certbot on Nginx, Caddy folds the certificate, the renewal cron, and the reverse-proxy config into a two-line Caddyfile.
In this guide, I’ll walk through a simple Docker-based setup where Caddy handles HTTPS and forwards traffic to services running inside a Docker network.
Basic Architecture
In this setup, all external traffic first reaches Caddy.
Caddy terminates TLS (HTTPS) and forwards requests internally over HTTP.
External User
│
▼
https://your-domain.example.com
│
▼
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ Caddy │────▶│ Frontend │─────▶│ Backend Services │
│ :80/:443 │ HTTP │ :8080 │ HTTP │ internal │
└──────────┘ └──────────────┘ └──────────────────┘
Prerequisites
Before starting, verify:
- Your domain points to your public server IP
- Router port forwarding is configured for ports 80 and 443
- Docker and Docker Compose are installed
- No other service is using ports 80 or 443
Project Structure
app-infra/
├── Caddyfile
├── docker-compose.yml
└── .env
Caddy Configuration
Create a Caddyfile:
your-domain.example.com {
reverse_proxy frontend:8080
}
Caddy will automatically:
- Issue a Let’s Encrypt certificate
- Enable HTTPS
- Redirect HTTP → HTTPS
- Renew certificates automatically
Docker Compose Configuration
Add the Caddy service:
services:
caddy:
container_name: app-caddy
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
frontend:
condition: service_healthy
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
The caddy_data volume stores TLS certificates so they persist across container restarts.
Starting the Services
cd /home/user/app-infra
docker compose up -d
Testing HTTPS
curl -I https://your-domain.example.com
Expected output:
HTTP/2 200
server: Caddy
Viewing Logs
docker compose logs -f caddy
Reloading Configuration
After editing the Caddyfile:
docker exec app-caddy caddy reload --config /etc/caddy/Caddyfile
or
docker compose restart caddy
Troubleshooting
Certificate issuance failed
Check:
nslookup your-domain.example.com
docker compose logs caddy
Ensure ports 80 and 443 are reachable externally. Port 80 is the one people forget: Caddy uses it for the HTTP→HTTPS redirect and for the ACME HTTP-01 challenge (one of two challenge types it enables). If 80 is blocked, Caddy can still fall back to the TLS-ALPN-01 challenge on 443 and issue the certificate — but you lose the redirect and a challenge path, so open both anyway. Confirm they’re reachable (UFW and your router). While you’re still fixing DNS or forwarding, point Caddy at Let’s Encrypt’s staging CA so a misconfigured domain doesn’t burn through the strict production rate limits.
502 Bad Gateway
Usually indicates the backend or frontend container is not responding.
docker compose ps frontend
docker compose logs frontend
Final Thoughts
Caddy is one of the simplest ways to enable HTTPS for Docker-based applications.
With automatic certificate management and minimal configuration, it significantly reduces operational complexity while providing production-ready TLS support.
Once the basic setup is complete, you can easily extend the configuration to support multiple domains, API routing, and WebSocket services.