Orchestrating Dynamic Reverse Proxying with Caddy and Docker

The integration of Caddy into a Dockerized environment represents a paradigm shift in how web servers and reverse proxies are managed. Traditionally, configuring a reverse proxy required manual edits to a configuration file, followed by a service restart or a signal to reload, which often introduced a risk of downtime or configuration drift. The synergy between Caddy and Docker—specifically when augmented by the caddy-docker-proxy plugin—transforms the Caddy server from a static piece of infrastructure into a dynamic, metadata-driven orchestration layer. By leveraging Docker labels, Caddy can automatically discover services, generate the necessary routing rules in-memory, and apply these changes in real-time without manual intervention. This architecture allows for a highly scalable environment where the desired state of the network is defined alongside the application code in a compose.yaml file, ensuring that infrastructure and application deployment remain perfectly synchronized.

The Architecture of Caddy Docker Proxy

The caddy-docker-proxy plugin is a sophisticated extension that enables Caddy to act as a reverse proxy for Docker containers by utilizing labels as the primary configuration mechanism. Instead of relying on a static Caddyfile stored on disk, the plugin actively monitors the Docker daemon's API for changes in container metadata.

When a container is started, stopped, or updated, the plugin scans the associated labels for specific keys starting with caddy. It then translates these labels into a corresponding Caddyfile configuration. For instance, a label such as caddy: service.example.com tells the proxy that any request for that domain should be routed to that specific container. The plugin automatically handles the mapping of the service to its internal DNS name or container IP address, removing the need for the administrator to track internal IP changes.

One of the most critical technical advantages of this system is the implementation of zero-downtime reloads. Every time a Docker object changes, the plugin updates the in-memory Caddyfile and triggers a graceful reload. This means that existing connections are maintained while new requests are routed according to the updated configuration, ensuring a seamless experience for the end-user.

Detailed Deployment Configurations

Implementing Caddy in a Docker environment requires careful attention to networking and volume persistence to ensure both performance and data integrity.

Network Configuration and IPv6 Considerations

A fundamental step in deploying Caddy with Docker is the creation of a dedicated network. The recommended approach involves using the following command:

docker network create caddy --ipv6

The inclusion of the --ipv6 flag is not merely optional but is a critical technical requirement for maintaining visibility into client traffic. In a standard Docker network without IPv6 enabled, Caddy and the upstream services it proxies will see the Docker gateway IP address instead of the actual client IP address for all IPv6 traffic. By enabling IPv6, the network ensures that the original client IP is preserved, which is essential for logging, security auditing, and implementing IP-based rate limiting or access control.

The Compose Specification

For a standard deployment, the compose.yaml file must define the Caddy service with specific ports and environment variables to facilitate the proxying mechanism.

```yaml
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
ports:
- 80:80
- 443:443/tcp
- 443:443/udp
environment:
- CADDYINGRESSNETWORKS=caddy
networks:
- caddy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- caddy_data:/data
restart: unless-stopped

networks:
caddy:
external: true

volumes:
caddy_data: {}
```

In this configuration, the ports 80 and 443 are exposed for standard HTTP and HTTPS traffic. Note that 443 is opened for both TCP and UDP to support HTTP/3, which utilizes the QUIC protocol. The CADDY_INGRESS_NETWORKS environment variable specifies which network Caddy should use to communicate with the backend containers.

Volume Management and Data Persistence

Caddy relies on persistent storage for several critical functions, most notably the management of SSL/TLS certificates.

The Role of caddy_data

The caddy_data volume is used to store certificates, keys, and other state information. Because Caddy automates the acquisition of certificates via Let's Encrypt or ZeroSSL, it must have a persistent location to store these certificates. If this data is not persisted via a Docker volume, certificates would be lost every time the container restarts, leading to frequent rate-limiting by certificate authorities and potential service outages.

Users can create this volume via the command line or through the Docker Desktop "Volumes" interface. When using the GUI, the user simply selects "Volumes," clicks "Create," and specifies the name caddy_data.

Interaction with the Admin API

Caddy features an administration API, typically exposed on port 2019. This API is vital for managing the server state without restarting the process. For local development, users may need to establish trust with the local Caddy instance by executing:

caddy trust --address localhost:2019

This action triggers a certificate confirmation dialog, allowing the local machine to trust the certificates generated by Caddy, which is essential for developing Single Page Applications (SPAs) or APIs over HTTPS in a local environment.

Operational Modes of Caddy Docker Proxy

The caddy-docker-proxy plugin offers three distinct execution modes to accommodate different architectural needs, ranging from simple single-host setups to complex Docker Swarm clusters.

Standalone Mode

In the default standalone mode, the Caddy instance acts as both the controller and the server. It monitors the Docker host socket, generates the configuration, and serves the traffic. This is the most common mode for developers and small-scale production environments as it requires no additional configuration.

Controller Mode

The controller mode is designed for distributed environments. In this mode, the Caddy instance acts exclusively as a manager. It monitors the Docker cluster, generates the necessary Caddy configuration, and pushes that configuration to all registered server instances.

  • Controller instances require direct access to the Docker host socket.
  • A single controller can manage multiple server instances across a cluster.
  • If the controller is connected to multiple networks, the CADDY_C_NETWORK environment variable or the controller-network CLI option must be used to define the primary communication channel.

Server Mode

In server mode, the Caddy instance acts solely as a data plane. It does not monitor the Docker socket and does not generate its own configuration. Instead, it waits for instructions from a controller.

  • Servers are marked as controllable using the label caddy_controlled_server.
  • They do not require access to the Docker host socket, allowing them to be deployed on worker nodes in a Swarm cluster where the Docker socket is not exposed.

Advanced Configuration and Custom Builds

While the official images provide extensive functionality, certain use cases require custom plugins or specific versions of Caddy.

Building Custom Images with xcaddy

The xcaddy tool is the official way to build Caddy with custom modules. To integrate caddy-docker-proxy into a custom build, a multi-stage Dockerfile is utilized:

dockerfile ARG CADDY_VERSION=2.6.1 FROM caddy:${CADDY_VERSION}-builder AS builder RUN xcaddy build \ --with github.com/lucaslorentz/caddy-docker-proxy/v2 \ --with <additional-plugins> FROM caddy:${CADDY_VERSION}-alpine COPY --from=builder /usr/bin/caddy /usr/bin/caddy CMD ["caddy", "docker-proxy"]

This process ensures that the final image remains lightweight (using the Alpine base) while containing all necessary compiled-in modules.

Docker Host Connectivity

Depending on the operating system, the connection to the Docker daemon differs. This is managed via the /var/run/docker.sock mount on Linux, but different protocols are used on other platforms:

  • Unix: unix:///var/run/docker.sock
  • Windows: npipe:////./pipe/docker_engine

To customize this connection, several environment variables can be employed:

  • DOCKER_HOST: Specifies the URL of the Docker server.
  • DOCKER_API_VERSION: Sets the specific API version to be used.
  • DOCKER_CERT_PATH: Defines the path to load TLS certificates for the Docker API.
  • DOCKER_TLS_VERIFY: Enables or disables TLS verification for the daemon connection.

Runtime Management and Optimization

Managing Caddy within Docker requires an understanding of how to interact with the running process and how to optimize the network stack.

Zero-Downtime Configuration Reloads

Caddy does not require a full container restart to apply configuration changes. Instead, it uses a reload command. To trigger this manually for a running container, the following sequence is used:

caddy_container_id=$(docker ps | grep caddy | awk '{print $1;}')
docker exec -w /etc/caddy $caddy_container_id caddy reload

This process identifies the container ID and executes the reload command within the /etc/caddy working directory, ensuring the new configuration is applied instantly without dropping active connections.

HTTP/3 and UDP Performance

Caddy includes native support for HTTP/3. Because HTTP/3 is based on QUIC (a UDP-based protocol), it can be hindered by default OS buffer sizes. The underlying quic-go library used by Caddy attempts to increase these buffer sizes to improve throughput and reduce packet loss, which is why opening port 443/udp in the Docker configuration is essential for performance.

Comparative Summary of Caddy Docker Integration

The following table summarizes the key components and their functions within the Caddy-Docker ecosystem.

Component Primary Function Key Requirement
caddy-docker-proxy Dynamic config generation via labels Docker socket access
caddy_data volume SSL certificate persistence External volume definition
Admin API (2019) Real-time server management Port mapping in compose
xcaddy Custom module compilation Builder stage in Dockerfile
HTTP/3 (QUIC) Low-latency web transport UDP port 443

Implementation Examples

To deploy a service that is automatically discovered by Caddy, the service definition in the compose.yaml must include the appropriate labels.

Example: Whoami Service Integration

For a service like whoami, the configuration would look as follows:

yaml services: whoami: image: traefik/whoami networks: - caddy labels: caddy: whoami.example.com caddy.reverse_proxy: {{upstreams}}

In this example, the label caddy: whoami.example.com defines the domain name. The caddy.reverse_proxy: {{upstreams}} label uses a template function that tells Caddy to automatically route traffic to the container's internal IP and port.

Conclusion

The integration of Caddy with Docker, particularly through the caddy-docker-proxy plugin, represents a sophisticated approach to infrastructure as code. By shifting the configuration from static files to dynamic Docker labels, administrators can achieve a level of agility that was previously impossible. The system's ability to handle IPv6 natively, persist critical SSL data through external volumes, and support high-performance protocols like HTTP/3 makes it a robust choice for modern microservices architectures. The distinction between controller and server modes further extends this utility to large-scale clusters, ensuring that the reverse proxy layer can scale horizontally alongside the application layer. Ultimately, this setup minimizes the operational overhead of managing SSL certificates and routing rules, allowing developers to focus on application logic while the infrastructure self-configures in real-time.

Sources

  1. caddy-docker-proxy GitHub
  2. Caddy Go Docker and SPA Guide
  3. Official Caddy Docker Image Sources
  4. Caddy Docker Hub Documentation

Related Posts