caddy-config uses Go's text/template engine to render site blocks. While standard reverse proxying and redirects work out-of-the-box, you can customize the entire site block for any instance.
When an instance has no custom template label, caddy-config applies the default vhost template (defaultVhostTemplate):
{{ .Domain }} {
{{- if .Redirect }}
redir {{ .Redirect }} permanent
{{- else if .Upstreams }}
reverse_proxy {{ range $i, $u := .Upstreams }}{{ if $i }} {{ end }}{{ $u }}{{ end }}
{{- end }}
}
By default, caddy-config prepends a minimal global options block (defaultBaseTemplate) to every rendered Caddyfile:
{
admin localhost:2019
}
You can overwrite this block to configure global settings such as TLS certificates, global logging, email, acme CA endpoints, or trusted proxies.
global_template)You specify a custom global template directly on each target using the global_template=<path> option within --caddy-instance or --os-path.
Relative paths and template names are resolved against --templates-dir (or the INCUS_CADDY_TEMPLATES_DIR environment variable) if specified, with optional .caddyfile or .tmpl extensions. Absolute paths are also supported directly.
As a template name in --templates-dir:
caddy-config run \
--templates-dir /etc/caddy/templates \
--caddy-instance external,instance=caddy-external,project=default,global_template=external.caddyfile \
--caddy-instance internal,instance=caddy-internal,project=default,global_template=internal.caddyfile
As an absolute file path:
caddy-config run \
--caddy-instance edge,instance=caddy,project=default,global_template=/etc/caddy/global.caddyfile
Co-located OS deployment:
caddy-config run \
--os-path edge,path=/etc/caddy/Caddyfile,global_template=/etc/caddy/global.caddyfile
Any deployment target that does not configure global_template uses the default minimal options block ({ admin localhost:2019 }).
The global template is evaluated with Go's text/template engine and has access to:
| Variable | Type | Description |
|---|---|---|
.Vhosts |
[]vhost |
List of all virtual hosts configured for this deployment target. |
Example:
{
admin localhost:2019
{{- if .Vhosts }}
# Configured for {{ len .Vhosts }} active routes
{{- end }}
}
Inside your custom template, the following fields are available:
| Variable | Type | Description |
|---|---|---|
.Domain |
string |
The domain(s) defined on the instance (user.label.<prefix>). |
.Service |
string |
The service name if defined (user.label.<prefix>.service or user.incus-compose.service). |
.Upstreams |
[]string |
Sorted list of resolved upstream addresses (e.g. ["10.0.1.5:8080", "10.0.1.6:8080"]). |
.Redirect |
string |
The redirect URL if configured via redir=<url> flag on domain. |
.Template |
string |
The raw template name or inline template string (template=<path> flag). |
.Flags |
map[string]string |
Custom flags parsed from label definitions (e.g. example.com,flag1=value1,resolvers='1.1.1.1 1.0.0.1'). Single or double quotes preserve spaces and commas. |
Custom templates are specified directly on the target label using the template=<value> flag:
--templates-dir)For reusable site configurations, store templates in a directory mounted to caddy-config and pass --templates-dir /etc/caddy/templates.
When an instance specifies a valid path with an extension (dir+file+ext, e.g. template=spa_site.caddyfile or template=spa_site.tmpl), caddy-config resolves it against --templates-dir (or uses an absolute path if provided). If the file does not exist, it fails with a 404/not-found error.
services:
web:
image: docker.io/library/nginx:alpine
labels:
edge: "spa.example.com,upstream=8080,template=spa_site.caddyfile"
Any template input without a file extension is evaluated as an inline Go template:
services:
web:
image: docker.io/library/nginx:alpine
labels:
edge: |
spa.example.com,upstream=8080,template={{ .Domain }} {
encode gzip zstd
reverse_proxy {{ index .Upstreams 0 }}
}
# /etc/caddy/templates/secure_proxy.caddyfile
{{ .Domain }} {
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
reverse_proxy {{ range $i, $u := .Upstreams }}{{ if $i }} {{ end }}{{ $u }}{{ end }}
}
Usage in Compose:
services:
app:
image: my-app:latest
labels:
edge: "secure.example.com,upstream=3000,template=secure_proxy.caddyfile"
Useful for single-page applications (React, Vue) or microservices serving static assets:
# /etc/caddy/templates/spa_app.caddyfile
{{ .Domain }} {
root * /var/www/dist
file_server
try_files {path} /index.html
handle /api/* {
reverse_proxy {{ index .Upstreams 0 }}
}
}
Connects directly to an upstream PHP container running php-fpm:
# /etc/caddy/templates/php_site.caddyfile
{{ .Domain }} {
root * /var/www/html
php_fastcgi {{ index .Upstreams 0 }}
file_server
}
Usage in Compose:
services:
wordpress:
image: docker.io/library/wordpress:fpm-alpine
labels:
edge: "blog.example.com,upstream=9000,template=php_site.caddyfile"
Optimized for real-time applications requiring continuous flushes and keep-alives:
# /etc/caddy/templates/websocket_proxy.caddyfile
{{ .Domain }} {
reverse_proxy {{ range $i, $u := .Upstreams }}{{ if $i }} {{ end }}{{ $u }}{{ end }} {
flush_interval -1
}
}
Every custom template is rendered in-memory and formatted/validated using the Caddy binary inside the container (caddy fmt --overwrite) before deployment.
If a custom template contains an invalid directive, bad syntax, or broken structure:
caddy fmt fails inside the container.caddy-config logs the exact error returned by Caddy.