Skip to content

Caddy Reverse Proxy

Caddy is the simplest option for automatic HTTPS. It handles Let's Encrypt certificates with zero configuration and is a great choice for home servers and Raspberry Pi deployments.


Prerequisites

  • Dreadnought running via Docker Compose (or systemd)
  • A domain name with DNS pointing to your server
  • Caddy installed

Installing Caddy

Ubuntu/Debian:

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

Or run as a Docker container (see below).


Step 1 — Set NEXT_PUBLIC_API_URL

Edit docker-compose.yml:

web:
  environment:
    - NEXT_PUBLIC_API_URL=https://ddns-api.yourdomain.com
    - NODE_ENV=production

Rebuild:

docker compose up -d --build

Step 2 — Configure the Caddyfile

Edit /etc/caddy/Caddyfile:

# Web UI
ddns.yourdomain.com {
    reverse_proxy localhost:8082
}

# API
ddns-api.yourdomain.com {
    reverse_proxy localhost:8081
}

That's it. Caddy automatically: - Obtains and renews Let's Encrypt certificates - Redirects HTTP to HTTPS - Sets secure TLS defaults


Step 3 — Reload Caddy

sudo systemctl reload caddy

# Check status
sudo systemctl status caddy

# Test configuration
caddy validate --config /etc/caddy/Caddyfile

Step 4 — Verify

curl -I https://ddns.yourdomain.com
curl https://ddns-api.yourdomain.com/health

Using Caddy on a Raspberry Pi

The same process works on a Pi. One extra step — open ports 80 and 443 if you're using UFW:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

And set up port forwarding on your router: external 443 → Pi's IP:443, and external 80 → Pi's IP:80 (needed for certificate issuance via HTTP challenge).


Running Caddy as a Docker Container

If you prefer to run Caddy in Docker alongside Dreadnought:

Create /opt/caddy/Caddyfile:

ddns.yourdomain.com {
    reverse_proxy dreadnought-ddns-web-1:3000
}

ddns-api.yourdomain.com {
    reverse_proxy dreadnought-ddns-api-1:8000
}

Create /opt/caddy/docker-compose.yml:

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"   # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - dreadnought

volumes:
  caddy_data:
  caddy_config:

networks:
  dreadnought:
    external: true
    name: dreadnought-ddns_default   # Matches Dreadnought's compose network

Start Caddy:

cd /opt/caddy
docker compose up -d

Find the Dreadnought network name:

docker network ls | grep dreadnought

Single Domain Setup (No Separate API Subdomain)

If you only have one domain and want both frontend and API under the same host:

ddns.yourdomain.com {
    # API requests go to the backend
    handle /api/* {
        reverse_proxy localhost:8081
    }

    handle /health {
        reverse_proxy localhost:8081
    }

    # Everything else goes to the frontend
    handle {
        reverse_proxy localhost:8082
    }
}

Set NEXT_PUBLIC_API_URL=https://ddns.yourdomain.com and rebuild.


Cloudflare DNS Challenge (No Ports 80/443 Required)

If you can't open ports (e.g. ISP blocks them, or you're using a carrier-grade NAT), Caddy supports the Cloudflare DNS challenge for certificate issuance. This requires the caddy-dns/cloudflare module.

Install Caddy with the Cloudflare plugin:

# Using xcaddy to build a custom Caddy binary
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build --with github.com/caddy-dns/cloudflare

Or use the Docker image with the plugin pre-installed:

services:
  caddy:
    image: ghcr.io/slothcroissant/caddy-cloudflaredns:latest
    # ... rest of config

Caddyfile with DNS challenge:

{
    acme_dns cloudflare your-cloudflare-api-token
}

ddns.yourdomain.com {
    reverse_proxy localhost:8082
}

ddns-api.yourdomain.com {
    reverse_proxy localhost:8081
}

Adding Security Headers

ddns.yourdomain.com {
    reverse_proxy localhost:8082

    header {
        X-Frame-Options DENY
        X-Content-Type-Options nosniff
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }
}

Troubleshooting Caddy

Certificate not issued

# Check logs
sudo journalctl -u caddy -f

# Common causes:
# - Port 80 not reachable from the internet (needed for HTTP challenge)
# - DNS not pointing to your server yet
# - Rate limit hit (too many cert requests for same domain)

502 Bad Gateway

Caddy can reach the site but the backend isn't responding:

docker compose ps   # Verify containers are running
curl http://localhost:8082   # Test frontend directly
curl http://localhost:8081/health   # Test API directly

Caddy config syntax error

caddy validate --config /etc/caddy/Caddyfile