Custom Vhost Templates

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.


Default Vhost Template

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 }}
}

Default Global Template & Customization

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.

Specifying a Global Template (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 }).

Global Template Context Variables

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 }}
}

Template Context Variables

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.

Defining Custom Templates

Custom templates are specified directly on the target label using the template=<value> flag:

1. External Template Directory (--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"

2. Inline Go Template

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 }}
        }

Production Template Examples

Example 1: Compression and Security Headers

# /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"

Example 2: Static Files with Reverse Proxy Fallback

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 }}
	}
}

Example 3: PHP-FPM FastCGI Site

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"

Example 4: WebSockets & Long-Lived Streaming

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
	}
}

Template Validation & Formatting Safety

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:

  1. caddy fmt fails inside the container.
  2. caddy-config logs the exact error returned by Caddy.
  3. The invalid temporary file is discarded.
  4. Caddy continues serving the existing, working configuration without interruption.