An excerpt from the repository, fetched Nov 13, 2026.
Read the project’s introduction
Caddy-Docker-Proxy
Introduction
This plugin enables Caddy to be used as a reverse proxy for Docker containers via labels.
How does it work?
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.
Table of contents
Basic usage example, using docker compose
Labels to Caddyfile conversion
Tokens and arguments
Ordering and isolation
Sites, snippets and global options
Go templates
Template functions
upstreams
Examples
Docker configs
Proxying services vs containers
Services
Containers
Execution modes
Server
Controller
Standalone (default)
Options
Docker images
Choosing the version numbers
Chosing between default or alpine images
CI images
ARM architecture images
Windows images
Custom images
Connecting to Docker Host
Volumes
Trying it
With Docker Compose (compose.yaml)
With run commands
Building it
Basic usage example (Docker Compose)
$ docker network create caddy --ipv6
NoteThe --ipv6 flag 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.
Labels to Caddyfile conversion
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:
Tokens and arguments
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
}
Ordering and isolation
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
}
Sites, snippets and global options
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
}
Go templates
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
Read the full README on GitHub