Skip to content

mcsdodo/caddy-multihost-agent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

54 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Caddy Multihost Agent

Extend caddy-docker-proxy to support containers across multiple hosts.

Why This Exists

caddy-docker-proxy is excellent for automatic reverse proxy configuration via Docker labels on a single host. However, it doesn't support multi-host setups - you can't route traffic to containers running on other machines.

caddy-multihost-agent solves this by:

  • Running agents on remote hosts that watch local Docker containers
  • Pushing route configurations to a central Caddy server via the Admin API
  • Using the same Docker label syntax as caddy-docker-proxy

This gives you caddy-docker-proxy's simplicity across your entire infrastructure.

Quick Start

Docker Image

docker pull mcsdodo/caddy-agent:latest

Single Host Setup

For a single host, just use caddy-docker-proxy directly. This agent is only needed for multi-host setups.

Multi-Host Setup

Host 1 (Server) - runs Caddy with caddy-docker-proxy:

# docker-compose.yml on host1
services:
  caddy:
    image: lucaslorentz/caddy-docker-proxy:latest
    container_name: caddy-server
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - caddy_data:/data
    labels:
      # Expose Admin API for remote agents
      caddy_0: ":2020"
      caddy_0.reverse_proxy: "localhost:2019"

  # Optional: agent for testing server mode
  caddy-agent:
    image: mcsdodo/caddy-agent:latest
    container_name: caddy-agent-server
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - CADDY_URL=http://localhost:2019
      - AGENT_ID=host1-server
      - DOCKER_LABEL_PREFIX=agent  # Use different prefix to avoid conflict
      - SNIPPET_API_PORT=8567      # Serve snippets to remote agents

volumes:
  caddy_data:

Host 2 (Remote Agent) - watches containers and pushes routes to host1:

# docker-compose.yml on host2
services:
  caddy-agent:
    image: mcsdodo/caddy-agent:latest
    container_name: caddy-agent-remote
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - CADDY_URL=http://192.168.1.10:2020   # Point to host1
      - AGENT_ID=host2-remote
      - DOCKER_LABEL_PREFIX=caddy
      - SNIPPET_SOURCES=http://192.168.1.10:8567  # Fetch snippets from host1

  # Example app - routes automatically configured
  myapp:
    image: nginx
    network_mode: host
    labels:
      caddy: myapp.example.com
      caddy.reverse_proxy: "{{upstreams 80}}"

Test it:

# From host1
curl -sk --resolve myapp.example.com:443:127.0.0.1 https://myapp.example.com

Architecture

host1 (192.168.1.10) - SERVER
├── Caddy (caddy-docker-proxy)
│   ├── Port 80/443 (routes)
│   └── Port 2020 (Admin API proxy → 2019)
└── Local containers (via caddy.* labels)
        ▲
        │ HTTP POST /load
    ┌───┴───┐
host2       host3
Agent       Agent
└── Local   └── Local
    containers  containers

Each agent:

  1. Watches its local Docker daemon for container events
  2. Extracts route config from caddy.* labels
  3. Pushes routes to the central Caddy server via Admin API
  4. Uses unique AGENT_ID prefixes to prevent route conflicts

Configuration

Environment Variables

Variable Default Description
CADDY_URL http://localhost:2019 Caddy Admin API URL
AGENT_ID hostname Unique identifier (prefixes route IDs)
DOCKER_LABEL_PREFIX caddy Label prefix to watch
HOST_IP auto-detected IP for upstream addresses
SNIPPET_API_PORT 0 (disabled) Port to serve snippet API
SNIPPET_SOURCES empty Comma-separated URLs to fetch snippets
SNIPPET_CACHE_TTL 300 Snippet cache duration in seconds
HEALTH_CHECK_INTERVAL 5 Seconds between health checks
RESYNC_INTERVAL 300 Seconds between full resyncs
CONFIG_PUSH_ENABLED true Set to false for snippet-only mode
LOG_LEVEL INFO Logging verbosity: DEBUG, INFO, WARNING, ERROR

Docker Labels

Standard caddy-docker-proxy labels work:

labels:
  # Basic reverse proxy
  caddy: example.com
  caddy.reverse_proxy: "{{upstreams 8080}}"

  # Multiple domains
  caddy: app1.example.com, app2.example.com
  caddy.reverse_proxy: "{{upstreams 8080}}"

  # HTTP-only (no TLS)
  caddy: http://internal.lan
  caddy.reverse_proxy: "{{upstreams 8080}}"

  # With snippet import
  caddy: secure.example.com
  caddy.import: internal
  caddy.reverse_proxy: "{{upstreams 8080}}"

  # Numbered labels for complex configs
  caddy_0: api.example.com
  caddy_0.reverse_proxy: "{{upstreams 8080}}"
  caddy_0.reverse_proxy.header_up: "X-Real-IP {remote_host}"

  # Port-based server (creates separate listener)
  caddy: ":3000"
  caddy.reverse_proxy: "{{upstreams 8080}}"

Layer4 (TCP/UDP) Routing

Route raw TCP/UDP traffic (not HTTP) through Caddy. Requires Caddy built with caddy-l4 module.

labels:
  # Simple TCP proxy (e.g., MQTT broker)
  caddy.layer4: ":1883"
  caddy.layer4.route.proxy: "{{upstreams 11883}}"

  # With SNI matching (TLS passthrough)
  caddy.layer4: ":443"
  caddy.layer4.@sni: "tls sni *.example.com"
  caddy.layer4.route.proxy: "192.168.1.50:443"

  # Multiple L4 routes (numbered)
  caddy.layer4_0: ":1883"
  caddy.layer4_0.route.proxy: "{{upstreams 1883}}"
  caddy.layer4_1: ":8883"
  caddy.layer4_1.route.proxy: "{{upstreams 8883}}"

Note: If Caddy doesn't have the Layer4 module, the agent logs a warning and skips L4 routes (HTTP routes still work).

Snippet Sharing

Define snippets on host1, use them on all hosts:

Host1 (serves snippets):

labels:
  caddy_1: (internal)
  caddy_1.tls: internal

  caddy_2: (https)
  caddy_2.reverse_proxy.transport: http
  caddy_2.reverse_proxy.transport.tls: ""
  caddy_2.reverse_proxy.transport.tls_insecure_skip_verify: ""

Remote agents fetch automatically when SNIPPET_SOURCES is configured.

Snippet compatibility with reverse_proxy:

Snippet Safe to import? Reason
internal Yes Only sets TLS cert type
https Yes Forces TLS to backend (for self-signed certs)
wildcard No Contains handle.abort - terminates request

Note: Wildcard domains (e.g., *.example.com) should import wildcard to set up TLS certs. Individual services should NOT import wildcard - just declare their domain and the cert is used automatically.

Snippet-only mode: To run an agent that only serves snippets without pushing any routes to Caddy, set CONFIG_PUSH_ENABLED=false:

caddy-agent:
  environment:
    - SNIPPET_API_PORT=8567
    - CONFIG_PUSH_ENABLED=false  # Disable route pushing, only serve snippets

Route Recovery

When Caddy restarts, routes are lost. Agents automatically recover:

  1. Health check (every 5s) - detects missing routes quickly
  2. Periodic resync (every 5 min) - fallback for network issues
  3. Exponential backoff - reduces load when Caddy is unreachable

Troubleshooting

Routes not appearing

# Check agent logs
docker logs caddy-agent-remote

# Check Caddy API is accessible from agent host
curl http://192.168.1.10:2020/config/

# Check container labels
docker inspect myapp | jq '.[0].Config.Labels'

Routes disappear after restart

Routes are pushed to Caddy's in-memory config. After Caddy restarts, agents resync within ~15 seconds (10s startup delay + 5s health check).

Connection refused to Admin API

Make sure port 2020 is exposed and accessible:

# Test from agent host
curl http://192.168.1.10:2020/config/

Duplicate AGENT_ID conflicts

Each agent must have a unique AGENT_ID. Routes are prefixed with this ID to prevent conflicts:

host1-server_myapp
host2-remote_webapp

Complete Example

Three-host setup with Cloudflare DNS:

Host1 (Server):

services:
  caddy:
    # Option A: Use caddy-docker-proxy (recommended for single-host)
    image: lucaslorentz/caddy-docker-proxy:latest
    # Option B: Use regular Caddy + caddy-agent (full control)
    # image: caddy:latest  # Requires caddy-agent with CONFIG_PUSH_ENABLED=true
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - caddy_data:/data
    labels:
      # Global config
      caddy_0.email: [email protected]
      caddy_0.auto_https: prefer_wildcard

      # Cloudflare DNS for wildcard certs
      caddy_1: (wildcard)
      caddy_1.tls.dns: "cloudflare {env.CF_API_TOKEN}"
      caddy_1.handle.abort: ""

      caddy_10: "*.example.com"
      caddy_10.import: wildcard

      # Admin API proxy for agents
      caddy_20: ":2020"
      caddy_20.reverse_proxy: "localhost:2019"

  caddy-agent:
    image: mcsdodo/caddy-agent:latest
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - CADDY_URL=http://localhost:2019
      - AGENT_ID=host1-server
      - SNIPPET_API_PORT=8567
      - CONFIG_PUSH_ENABLED=false  # Snippet-only mode

volumes:
  caddy_data:

Host2/Host3 (Agents):

services:
  caddy-agent:
    image: mcsdodo/caddy-agent:latest
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - CADDY_URL=http://192.168.1.10:2020
      - AGENT_ID=host2-remote  # Change for each host
      - SNIPPET_SOURCES=http://192.168.1.10:8567

  myapp:
    image: myapp:latest
    network_mode: host
    labels:
      caddy: myapp.example.com
      caddy.reverse_proxy: "{{upstreams 8080}}"

FAQ

Q: Can I use this without caddy-docker-proxy? A: Yes, but you'll need a Caddyfile that enables the Admin API and defines your base config.

Q: What happens if two containers have the same domain? A: The agent overwrites with the latest one. Use unique domains.

Q: Does it work with Docker Swarm or Kubernetes? A: Not directly. For orchestrated environments, use native ingress controllers.

Q: How do I secure the Admin API? A: Use firewall rules to restrict access to port 2020 to agent IPs only.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •