Caddy is a web server that obtains and renews HTTPS certificates automatically — no Certbot, no cron. Its config file, the Caddyfile, is so short that a reverse proxy for a container is two lines. Running Caddy itself in Docker alongside your apps is a clean way to publish many services on one VPS.

1. Shared network

docker network create web

Every app that Caddy should proxy joins this network; apps don’t publish ports.

2. Caddy’s Compose file

mkdir -p ~/caddy && cd ~/caddy

compose.yaml:

services:
  caddy:
    image: caddy:2
    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: [web]

volumes:
  caddy_data:
  caddy_config:

networks:
  web:
    external: true

The caddy_data volume stores certificates and keys — keep it (don’t recreate it on every deploy), otherwise Caddy requests new certificates each time and can hit Let’s Encrypt rate limits.

3. The Caddyfile

{
	email [email protected]
}

app.example.com {
	reverse_proxy app:3000
}

uptime.example.com {
	reverse_proxy uptime-kuma:3001
}

files.example.com {
	reverse_proxy filebrowser:80
	encode zstd gzip
}

app:3000 is the container (service) name and its internal port on the shared web network. That’s the whole configuration: Caddy redirects HTTP to HTTPS, obtains certificates, renews them, enables HTTP/2 and HTTP/3, and forwards the right headers (X-Forwarded-For, X-Forwarded-Proto, Host). WebSockets work automatically.

docker compose up -d
docker compose logs -f caddy

DNS for each hostname must point to the server before Caddy can get certificates.

4. An app joining the network

# ~/apps/uptime-kuma/compose.yaml
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    networks: [web]

networks:
  web:
    external: true

No ports: — the app is only reachable through Caddy.

5. Reload after editing the Caddyfile

docker compose exec -w /etc/caddy caddy caddy reload

Zero downtime; if the new config is invalid, Caddy keeps the old one and prints the error. Format and validate:

docker compose exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile   # needs a writable mount
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile

Useful directives

Basic auth for an admin tool:

docker compose exec caddy caddy hash-password --plaintext 'secret'
admin.example.com {
	basic_auth {
		alice $2a$14$…hash…
	}
	reverse_proxy portainer:9000
}

IP restriction:

internal.example.com {
	@blocked not remote_ip 203.0.113.0/24
	respond @blocked 403
	reverse_proxy grafana:3000
}

Security headers:

(security) {
	header {
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
}

app.example.com {
	import security
	reverse_proxy app:3000
}

Static files alongside:

example.com {
	root * /srv/site
	file_server
}

(Mount /srv/site into the Caddy container.)

Upload size / timeouts — Caddy has no default body-size limit; add request_body { max_size 100MB } if you want one.

Wildcard certificates

Wildcard certs need a DNS provider module — use a custom Caddy build (the caddy:builder image with xcaddy build --with github.com/caddy-dns/cloudflare) and the tls { dns cloudflare {env.CF_API_TOKEN} } directive.

Logs

app.example.com {
	log {
		output file /data/logs/app.log
		format json
	}
	reverse_proxy app:3000
}

Caddy vs Traefik vs Nginx Proxy Manager

  • Caddy — one readable config file, automatic HTTPS, very little to learn.
  • Traefik — configuration via container labels; good when apps change often.
  • Nginx Proxy Manager — web UI.

Prefer Caddy without Docker? See Caddy as a reverse proxy. A Docker VPS with Caddy in front can host many self-hosted apps on a single IP.

  • Caddy
  • Docker
  • Reverse Proxy