Software Engineer's Blog

Setting Up Caddy Reverse Proxy with Automatic HTTPS (Docker)

Setting Up Caddy Reverse Proxy with Automatic HTTPS (Docker)

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.