Caddy Docker Setup Guide

This guide explains how to set up Caddy as a reverse proxy with Cloudflare DNS integration for automatic SSL certificates.

Prerequisites

  • Docker and Docker Compose installed on your remote server
  • Cloudflare account with your domain configured
  • Cloudflare API token with Zone:DNS:Edit permissions

Directory Structure

Create the following directory structure on your remote server:

mkdir -p /opt/caddy/{config,data,logs}
cd /opt/caddy

1. Create Dockerfile

Create /opt/caddy/Dockerfile:

FROM caddy:2-builder AS builder

RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:2-alpine

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

2. Create docker-compose.yml

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

version: '3.8'

services:
  caddy:
    build: .
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "2019:2019" # Admin API - SECURE THIS!
    environment:
      - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
      - ACME_EMAIL=${ACME_EMAIL}
      - CADDY_ADMIN_KEY=${CADDY_ADMIN_KEY}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data:/data
      - ./config:/config
      - ./logs:/logs
    networks:
      - caddy

networks:
  caddy:
    external: true

3. Create Initial Caddyfile

Create /opt/caddy/Caddyfile:

{
    # Global options
    admin :2019 {
        origins localhost
        api_key {$CADDY_ADMIN_KEY}
    }
    
    # ACME DNS challenge via Cloudflare
    acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
    email {$ACME_EMAIL}
    
    # Logging
    log {
        output file /logs/caddy.log
        level INFO
    }
}

# Default site (catches all unmatched requests)
:443 {
    respond "No site configured for this domain" 404
}

# Health check endpoint
:2019/health {
    respond "OK" 200
}

4. Create Environment File

Create /opt/caddy/.env:

# Cloudflare API Token (with Zone:DNS:Edit permissions)
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token_here

# Email for Let's Encrypt notifications
ACME_EMAIL=ssl@yourdomain.com

# API Key for Caddy Admin API
CADDY_ADMIN_KEY=generate_a_secure_api_key_here

Important: Generate a secure API key:

openssl rand -hex 32

5. Secure the Admin API

# Allow only specific IPs to access the Admin API
sudo ufw allow from YOUR_IP_ADDRESS to any port 2019

# Or use iptables
sudo iptables -A INPUT -p tcp --dport 2019 -s YOUR_IP_ADDRESS -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 2019 -j DROP

Option B: VPN/Tailscale Access

Install Tailscale on the server and only expose port 2019 on the Tailscale interface:

# In docker-compose.yml, modify ports:
ports:
  - "80:80"
  - "443:443"
  - "100.64.0.1:2019:2019" # Tailscale IP only

Option C: SSH Tunnel

Don't expose port 2019 publicly and use SSH tunnel:

# From your local machine
ssh -L 2019:localhost:2019 user@server

6. Create Docker Network

docker network create caddy

7. Build and Start Caddy

cd /opt/caddy
docker-compose build
docker-compose up -d

8. Verify Installation

# Check if Caddy is running
docker-compose ps

# Check logs
docker-compose logs -f

# Test Admin API (from allowed IP)
curl -H "Authorization: Bearer YOUR_API_KEY" https://your-server:2019/config/

9. Configure Systemd (Optional)

Create /etc/systemd/system/caddy-docker.service:

[Unit]
Description=Caddy Docker Container
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/caddy
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

Enable the service:

sudo systemctl enable caddy-docker
sudo systemctl start caddy-docker

10. Configure Your App

In your local app, configure the Caddy settings:

  • Caddy API URL: https://your-server:2019 (or via tunnel)
  • API Key: The key you generated in step 4
  • Cloudflare API Token: Your Cloudflare token

Security Best Practices

  1. Never expose port 2019 to the public internet
  2. Use strong API keys (32+ characters)
  3. Regularly update Caddy and Docker images
  4. Monitor logs for suspicious activity
  5. Use fail2ban to protect against brute force
  6. Enable Docker's built-in TLS for the daemon
  7. Consider using Caddy's built-in basic auth for additional security

Troubleshooting

Certificate Issues

Check Caddy logs:

docker-compose logs caddy | grep -i error

API Connection Issues

Test from the server:

curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:2019/config/

DNS Validation Failed

Ensure your Cloudflare API token has the correct permissions:

  • Zone:DNS:Edit
  • Zone:Zone:Read

Maintenance

Update Caddy

cd /opt/caddy
docker-compose pull
docker-compose build
docker-compose up -d

Backup Configuration

# Backup data and config
tar -czf caddy-backup-$(date +%Y%m%d).tar.gz data/ config/

View Certificates

docker exec caddy caddy list-certificates

Example Route Configuration

Once connected to your app, routes will be automatically configured via the Admin API. Here's an example of what gets pushed:

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443"],
          "routes": [{
            "match": [{"host": ["app.example.com"]}],
            "handle": [{
              "handler": "reverse_proxy",
              "upstreams": [{"dial": "10.0.0.10:3000"}],
              "health_checks": {
                "active": {
                  "path": "/health",
                  "interval": "30s"
                }
              }
            }]
          }]
        }
      }
    }
  }
}

On this page