This plugin enables Caddy to be used as a reverse proxy for Docker containers via labels.
The plugin scans Docker metadata, looking for labels indicating that the service or container should be served by Caddy.
Then, it generates an in-memory Caddyfile with site entries and proxies pointing to each Docker service by their DNS name or container IP.
Every time a Docker object changes, the plugin updates the Caddyfile and triggers Caddy to gracefully reload, with zero-downtime.
$ docker network create caddy --ipv6
[!NOTE] The
--ipv6flag instructs Docker to assign IPv6 addresses for all containers connected to this network. Without this flag, Caddy (as well as any upstream services) will see Docker's gateway IP address instead of the actual client IP addresses for IPv6 clients.
caddy/compose.yaml
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
ports:
- 80:80
- 443:443/tcp
- 443:443/udp
environment:
- CADDY_INGRESS_NETWORKS=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: {}
$ docker compose up -d
whoami/compose.yaml
services:
whoami:
image: traefik/whoami
networks:
- caddy
labels:
caddy: whoami.example.com
caddy.reverse_proxy: "{{upstreams 80}}"
networks:
caddy:
external: true
$ docker compose up -d
Now, visit https://whoami.example.com. The site will be served automatically over HTTPS with a certificate issued by Let's Encrypt or ZeroSSL.
Please first read the Caddyfile Concepts documentation to understand the structure of a Caddyfile.
Any label prefixed with caddy will be converted into a Caddyfile config, following these rules:
Keys are the directive name, and values are whitespace separated arguments:
caddy.directive: arg1 arg2
↓
{
directive arg1 arg2
}
If you need whitespace or line-breaks inside one of the arguments, use double-quotes or backticks around it:
caddy.respond: / "Hello World" 200
↓
{
respond / "Hello World" 200
}
caddy.respond: / `Hello\nWorld` 200
↓
{
respond / `Hello
World` 200
}
caddy.respond: |
/ `Hello
World` 200
↓
{
respond / `Hello
World` 200
}
Dots represent nesting, and grouping is done automatically:
caddy.directive: argA
caddy.directive.subdirA: valueA
caddy.directive.subdirB: valueB1 valueB2
↓
{
directive argA {
subdirA valueA
subdirB valueB1 valueB2
}
}
Arguments for the parent directive are optional (e.g. no arguments to directive, setting subdirective subdirA directly):
caddy.directive.subdirA: valueA
↓
{
directive {
subdirA valueA
}
}
Labels with empty values generate a directive without any arguments:
caddy.directive:
↓
{
directive
}
Be aware that directives are subject to be sorted according to the default directive order defined by Caddy, when the Caddyfile is parsed (after the Caddyfile is generated from labels).
Directives from labels are ordered alphabetically by default:
caddy.bbb: value
caddy.aaa: value
↓
{
aaa value
bbb value
}
Suffix _<number> isolates directives that otherwise would be grouped:
caddy.route_0.a: value
caddy.route_1.b: value
↓
{
route {
a value
}
route {
b value
}
}
Prefix <number>_ isolates directives but also defines a custom ordering for directives (mainly relevant within route blocks), and directives without order prefix will go last:
caddy.1_bbb: value
caddy.2_aaa: value
caddy.3_aaa: value
↓
{
bbb value
aaa value
aaa value
}
A label caddy creates a site block:
caddy: example.com
caddy.respond: "Hello World" 200
↓
example.com {
respond "Hello World" 200
}
Or a snippet:
caddy: (encode)
caddy.encode: zstd gzip
↓
(encode) {
encode zstd gzip
}
It's also possible to isolate Caddy configurations using suffix _<number>:
caddy_0: (snippet)
caddy_0.tls: internal
caddy_1: site-a.com
caddy_1.import: snippet
caddy_2: site-b.com
caddy_2.import: snippet
↓
(snippet) {
tls internal
}
site_a {
import snippet
}
site_b {
import snippet
}
Global options can be defined by not setting any value for caddy. They can be set in any container/service, including caddy-docker-proxy itself. Here is an example
caddy.email: you@example.com
↓
{
email you@example.com
}
Named matchers can be created using @ inside labels:
caddy: localhost
caddy.@match.path: /sourcepath /sourcepath/*
caddy.reverse_proxy: @match localhost:6001
↓
localhost {
@match {
path /sourcepath /sourcepath/*
}
reverse_proxy @match localhost:6001
}
Golang templates can be used inside label values to increase flexibility. From templates, you have access to current Docker resource information. But, keep in mind that the structure that describes a Docker container is different from a service.
While you can access a service name like this:
caddy.respond: /info "{{.Spec.Name}}"
↓
respond /info "myservice"
The equivalent to access a container name would be:
caddy.respond: /info "{{index .Names 0}}"
↓
respond /info "mycontainer"
Sometimes it's not possile to have labels with empty values, like when using some UI to manage Docker. If that's the case, you can also use our support for go lang templates to generate empty labels.
caddy.directive: {{""}}
↓
directive
The following functions are available for use inside templates:
Returns all addresses for the current Docker resource separated by whitespace.
For services, that would be the service DNS name when proxy-service-tasks is false, or all running tasks IPs when proxy-service-tasks is true.
For containers, that would be the container IPs.
Only containers/services that are connected to Caddy ingress networks are used.
:warning: caddy docker proxy does a best effort to automatically detect what are the ingress networks. But that logic fails on some scenarios: #207. To have a more resilient solution, you can manually configure Caddy ingress network using CLI option ingress-networks, environment variable CADDY_INGRESS_NETWORKS. You can also specify the ingress network per container/service by adding to it a label caddy_ingress_network with the network name.
Usage: upstreams [http|https] [port]
Examples:
caddy.reverse_proxy: {{upstreams}}
↓
reverse_proxy 192.168.0.1 192.168.0.2
caddy.reverse_proxy: {{upstreams https}}
↓
reverse_proxy https://192.168.0.1 https://192.168.0.2
caddy.reverse_proxy: {{upstreams 8080}}
↓
reverse_proxy 192.168.0.1:8080 192.168.0.2:8080
caddy.reverse_proxy: {{upstreams http 8080}}
↓
reverse_proxy http://192.168.0.1:8080 http://192.168.0.2:8080
:warning: Be carefull with quotes around upstreams. Quotes should only be added when using yaml.
caddy.reverse_proxy: "{{upstreams}}"
↓
reverse_proxy "192.168.0.1 192.168.0.2"
Proxying all requests to a domain to the container
caddy: example.com
caddy.reverse_proxy: {{upstreams}}
Proxying all requests to a domain to a subpath in the container
caddy: example.com
caddy.rewrite: * /target{path}
caddy.reverse_proxy: {{upstreams}}
Proxying requests matching a path
caddy: example.com
caddy.handle: /source/*
caddy.handle.0_reverse_proxy: {{upstreams}}
Proxying requests matching a path, while stripping that path prefix
caddy: example.com
caddy.handle_path: /source/*
caddy.handle_path.0_reverse_proxy: {{upstreams}}
Proxying requests matching a path, rewriting to different path prefix
caddy: example.com
caddy.handle_path: /source/*
caddy.handle_path.0_rewrite: * /target{uri}
caddy.handle_path.1_reverse_proxy: {{upstreams}}
Proxying all websocket requests, and all requests to /api*, to the container
caddy: example.com
caddy.@ws.0_header: Connection *Upgrade*
caddy.@ws.1_header: Upgrade websocket
caddy.0_reverse_proxy: @ws {{upstreams}}
caddy.1_reverse_proxy: /api* {{upstreams}}
Proxying multiple domains, with certificates for each
caddy: example.com, example.org, www.example.com, www.example.org
caddy.reverse_proxy: {{upstreams}}
Redirecting
caddy: example.com
caddy.redir_0: /favicon.ico /alternative/icon.ico 302
caddy.redir_1: /photo.png /updated-photo.png 302
More community-maintained examples are available in the Wiki.
Note: This is for Docker Swarm only. Alternatively, use
CADDY_DOCKER_CADDYFILE_PATHor-caddyfile-path
You can also add raw text to your Caddyfile using Docker configs. Just add Caddy label prefix to your configs and the whole config content will be inserted at the beginning of the generated Caddyfile, outside any server blocks.
Caddy docker proxy is able to proxy to swarm services or raw containers. Both features are always enabled, and what will differentiate the proxy target is where you define your labels.
To proxy swarm services, labels should be defined at service level. With your compose.yaml, labels should be inside deploy, like:
services:
foo:
deploy: # <-- labels should be _inside_ `deploy`
labels:
caddy: service.example.com
caddy.reverse_proxy: {{upstreams}}
Caddy will use service DNS name as target or all service tasks IPs, depending on configuration proxy-service-tasks.
To proxy containers, labels should be defined at container level. With your compose.yaml, labels should be outside deploy, like:
services:
foo:
labels:
caddy: service.example.com
caddy.reverse_proxy: {{upstreams}}
Each caddy docker proxy instance can be executed in one of the following modes.
Acts as a proxy to your Docker resources. The server starts without any configuration, and will not serve anything until it is configured by a "controller".
In order to make a server discoverable and configurable by controllers, you need to mark it with label caddy_controlled_server and define the controller network via CLI option controller-network or environment variable CADDY_CONTROLLER_NETWORK.
Server instances doesn't need access to Docker host socket and you can run it in manager or worker nodes.
Controller monitors your Docker cluster, generates Caddy configuration, and pushes it to all servers it finds in your Docker cluster.
When controller instances are connected to more than one network, it is also necessary to define the controller network via CLI option controller-network or environment variable CADDY_CONTROLLER_NETWORK.
Controller instances require access to Docker host socket.
A single controller instance can configure all server instances in your cluster.
**:warning: Controller mode requires server nodes to serve
$ claude mcp add caddy-docker-proxy \
-- python -m otcore.mcp_server <graph>