caddy-config derives routing rules from labels assigned to Incus containers and virtual machines.
caddy-config connects a label prefix to a target Caddy server using --caddy-instance (for Incus containers/VMs) or --os-path (for local OS deployment):
--caddy-instance <label>:<project>:<instance>
--os-path [<label>:]<path>
For example:
# Remote Incus instance
--caddy-instance edge:default:caddy-prod
# Local OS Caddyfile (defaults to label "caddy")
--os-path /etc/caddy/Caddyfile
# Local OS Caddyfile with custom label prefix
--os-path edge:/etc/caddy/Caddyfile
You can specify --caddy-instance and --os-path multiple times to route different subsets of services to distinct Caddy targets.
In Incus, user-defined labels carry the user.label. prefix.
When using incus-compose, the user.label. prefix is automatically added to all entries under the labels: section.
In compose.yaml |
In Incus (incus config show <instance>) |
|---|---|
labels: edge.domain: "example.com" |
user.label.edge.domain: "example.com" |
labels: edge.upstream: "8080" |
user.label.edge.upstream: "8080" |
For a target bound to prefix edge:
| Label | Description | Example |
|---|---|---|
user.label.edge.domain |
(Required) Domain name(s) to match. Multiple domains are separated by spaces. | api.example.com or app.lan web.lan |
user.label.edge.upstream |
Target port or host:port override. If omitted, routes to container IP on default HTTP port. |
8080, 3000, or 10.0.1.50:9090 |
user.label.edge.network |
Incus network interface name to resolve IPv4 from. Defaults to the first valid non-loopback IPv4 address. | incusbr0, eth0, or internal |
user.label.edge.redirect |
Target URL for permanent redirects (renders redir <url> permanent). |
https://example.com{uri} |
user.label.edge.template |
Custom vhost template name in --custom-templates-dir or an inline Go template. |
php_site or inline site block |
user.label.edge.service |
Custom service name override (defaults to user.label.incus-compose.service). |
payments-api |
caddy-config resolves upstream container IP addresses dynamically:
user.label.<prefix>.network is set (e.g. internal), caddy-config searches the instance's interfaces for one attached to internal and selects its first non-loopback IPv4 address.caddy-config selects the first non-loopback IPv4 address across all attached interfaces.127.0.0.0/8 (loopback) or unparseable IP addresses are automatically skipped.Routes http://web.example.test to port 8080 of the web container:
services:
web:
image: docker.io/library/nginx:alpine
labels:
edge.domain: "web.example.test"
edge.upstream: "8080"
Rendered Caddyfile:
web.example.test {
reverse_proxy 10.0.1.15:8080
}
To match multiple domains for the same service, separate them with spaces:
services:
portal:
image: docker.io/library/nginx:alpine
labels:
edge.domain: "portal.example.com app.example.com"
edge.upstream: "80"
Rendered Caddyfile:
portal.example.com app.example.com {
reverse_proxy 10.0.1.18:80
}
When multiple instances define the same edge.domain, caddy-config merges their upstreams into a single sorted load-balanced reverse_proxy directive:
services:
api1:
image: docker.io/library/busybox:latest
command: httpd -f -p 8080
labels:
edge.domain: "api.example.com"
edge.upstream: "8080"
api2:
image: docker.io/library/busybox:latest
command: httpd -f -p 8080
labels:
edge.domain: "api.example.com"
edge.upstream: "8080"
Rendered Caddyfile:
api.example.com {
reverse_proxy 10.0.1.20:8080 10.0.1.21:8080
}
If api1 stops, caddy-config detects the stop event and updates Caddy to route solely to api2. When api1 restarts, it is automatically restored to the pool.
To redirect one domain to another:
services:
old-site:
image: docker.io/library/busybox:latest
command: sh -c "sleep infinity"
labels:
edge.domain: "old.example.com"
edge.redirect: "https://new.example.com{uri}"
Rendered Caddyfile:
old.example.com {
redir https://new.example.com{uri} permanent
}
If an instance is connected to both a private management network (mgmt) and an internal service bridge (appbr0), explicitly pick the interface for reverse proxying:
services:
backend:
image: docker.io/library/nginx:alpine
labels:
edge.domain: "backend.internal"
edge.upstream: "8000"
edge.network: "appbr0"