Caddy’s Docker module doesn’t actually discover containers; it’s Caddy itself that needs to be told to look for them.

Let’s see it in action. Imagine you have a web application running in a Docker container, and you want Caddy to automatically serve it.

Here’s a typical setup:

docker-compose.yml:

version: '3.8'

services:
  my-app:
    image: nginx:alpine # Replace with your actual app image
    labels:
      - "caddy.reverse_proxy=my-app:80" # This label tells Caddy to proxy to this container
      - "caddy.tls.challenge.provider=dns"
      - "caddy.tls.challenge.dns.provider=cloudflare"
      - "caddy.tls.challenge.dns.cloudflare.api_token=${CLOUDFLARE_API_TOKEN}"
    networks:
      - caddy-net

  caddy:
    image: caddy:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile # We'll configure Caddy here
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock # Caddy needs access to the Docker socket
    networks:
      - caddy-net
    depends_on:
      - my-app

networks:
  caddy-net:

volumes:
  caddy_data:
  caddy_config:

In this docker-compose.yml:

  • my-app is our example application service.
  • The labels on my-app are the crucial part. Caddy’s Docker module watches for containers with specific labels.
    • caddy.reverse_proxy=my-app:80: This tells Caddy to set up a reverse proxy to the my-app container, listening on port 80 within that container.
    • The caddy.tls.challenge... labels are for automatic HTTPS using DNS challenges (here, Cloudflare). Caddy will use these to obtain and renew TLS certificates for your domain.
  • The caddy service itself:
    • It exposes ports 80 and 443.
    • It mounts the Docker socket (/var/run/docker.sock) so it can communicate with the Docker daemon.
    • It’s connected to the caddy-net network, allowing it to reach my-app by its service name.

Caddyfile:

{
    # Enable the Docker discovery module
    # This tells Caddy to scan for containers with Caddy labels
    auto_https
    docker_auto_discovery {
        # Optional: specify a domain filter if needed
        # domain example.com
    }
}

# This Caddyfile config is actually NOT needed when using docker_auto_discovery
# Caddy will generate the configuration based on container labels.
# However, you might need global options here.
# For example, to set up your DNS provider credentials for auto-TLS:
# (This is often better handled via environment variables passed to the Caddy container)

When you run docker-compose up -d, Caddy starts, connects to the Docker daemon, and polls for containers. It sees my-app, inspects its labels, and dynamically configures itself to proxy traffic to my-app:80. If my-app is set up for HTTPS (which it is in the example with DNS challenge labels), Caddy will also procure and manage TLS certificates.

The core problem this solves is eliminating manual configuration for every new service you deploy. Instead of updating Caddyfile for each new microservice, you just add the correct Docker labels to the service’s definition. Caddy, running as a separate container, watches the Docker API for these labels and configures itself on the fly. This is powerful for dynamic environments where services spin up and down frequently.

The most surprising thing is that the docker_auto_discovery directive is a global option, not a site block. This means it influences how Caddy discovers all potential sites it should serve, rather than configuring a specific domain. It acts as a listener for the Docker API, and when it finds a container with relevant labels, it generates the site config for that container internally.

What most people don’t realize is how granular you can get with those labels. You can specify not just the reverse_proxy target but also header_up, header_down, tls settings per container, and even override global options like tls.automation or log settings on a per-container basis. This allows you to tailor Caddy’s behavior for individual services without touching the main Caddyfile. For instance, caddy.reverse_proxy.header.X-Forwarded-Proto=https on a container’s labels will ensure that header is sent upstream, even if Caddy is receiving traffic on HTTP for that specific proxy.

The next thing you’ll likely run into is managing more complex routing scenarios, like having multiple services behind a single Caddy instance that need different domain names or path-based routing.

Want structured learning?

Take the full Caddy course →