Reverse Proxy: Caddy
Deploy Caddy server. See Reverse Proxy for the
implementation-agnostic overview, the shared variables, and the cs_rp_which_one switch that
selects Caddy.
Ansible hosts group: reverse_proxy
Variables: shared
Shared variables are documented on the Reverse Proxy page.
Variables
| Option | Type | Description | Default |
|---|---|---|---|
cs_rp_caddy_cluster | string | Cluster name for the proxy | {{ cs_cluster_name }} |
cs_rp_caddy_directory | string | Directory for data and configuration | /app/caddy |
cs_rp_caddy_cluster_dns | list[string] | DNS servers for the container | The cluster's DNS servers (from Vault) |
cs_rp_caddy_version | string | Caddy version | 2.11.7-alpine |
cs_rp_caddy_docker_image | string | Caddy docker image | {{ cs_vm_artifact_registry_containers_home }}/caddy |
cs_rp_hosts.<Application Name>
Host definitions are sourced from cs_rp_hosts (see
Reverse Proxy), defined per host in inventory.yml. The
following keys are honored by the Caddy deployment; keys marked "not applied" are accepted for
compatibility but have no effect under Caddy.
| Option | Type | Description | Applied |
|---|---|---|---|
domains | list[string] | Domains for the application | Yes |
forward_host | string | Application hostname or IP | Yes |
forward_port | int | Application port | Yes |
forward_scheme | string | http or https | Yes |
hsts_enabled | bool | Enable HSTS (default true) | Yes |
hsts_subdomains | bool | Enable HSTS for subdomains (default true) | Yes |
enabled | bool | Enable the application (default true) | Yes |
mtls_outbound | bool | Require and verify client certificates (default true) | Yes |
ssl_forced | bool | Force SSL (default true) | Not applied: Caddy always auto-redirects HTTP to HTTPS for these sites |
allow_websocket_upgrade | bool | Allow websocket upgrade (default false) | Not applied: Caddy proxies WebSocket upgrades automatically |
http2_support | bool | Enable HTTP/2 support (default true) | Not applied: HTTP/2 and HTTP/3 are always enabled |
caching_enabled | bool | Enable caching (default true) | Not applied: no equivalent |
block_exploits | bool | Block common exploits (default true) | Not applied: no equivalent |
Vault configurations
- key:
{{ cs_project_code }}/application-deployer/clusters/{{ Lab Name or Cluster name }}/hosts/{{ hostname }}/apps/reverse-proxy/config
Default certificates, the admin API management port, and optional per-application certificate
overrides are read from Vault; proxy host definitions themselves come from cs_rp_hosts (see above).
{
"certificate": "Mandatory. Default certificate for the reverse proxy.",
"private_key": "Mandatory. Default private key for the reverse proxy.",
"management_port": "<Int port for admin API management console.>",
"apps": {
"<Application Name, matching a key in cs_rp_hosts>": {
"certificate": "Optional. Certificate for the application.",
"private_key": "Optional. Private key for the certificate."
}
}
}
Top-level certificate and private_key are mandatory default certificates for the reverse
proxy. If certificate/private_key are not provided for an application in apps, the default
top-level certificate and private_key are used.
Configuration layout
{{ cs_rp_caddy_directory }}/Caddyfile holds the global log options and two reusable
Caddyfile snippets: (mtls) and
(access_policy), followed by import /etc/caddy/sites/*. Each enabled application gets its
own file at {{ cs_rp_caddy_directory }}/sites/<Application Name>.caddy, which imports the
shared snippets rather than repeating their logic.
Access list
The (access_policy) snippet defines a remote_ip matcher built once from the addresses in
cs_rp_cidr_allow_list. Every site imports it; requests that do not match any allowed
address receive a 403 response.
Logging
Every log is written to both the console (docker logs {{ cs_rp_container_name }}) and a file under
{{ cs_rp_caddy_directory }}/logs/:
| Log | Contents | File |
|---|---|---|
| Application log | Caddy's own startup, TLS, and admin API events | logs/caddy.log |
| Per-site combined log | Every request handled by that site, success and error alike | logs/sites/<Application Name>/access.log |
| Per-site error log | Only requests Caddy logged at ERROR level (5xx responses) | logs/sites/<Application Name>/error.log |
The error-only log uses Caddy's built-in log-level filtering (level ERROR on a dedicated logger)
rather than a custom status matcher: the site-level log directive has no matcher support, and
4xx responses are logged at INFO, not a distinguishable level, so the error log necessarily
covers 5xx-class failures. 4xx responses still appear in the combined per-site log.
Health check
The container's Docker healthcheck queries the Caddy admin API at
http://127.0.0.1:{{ management_port }}/config/ from inside the container. Before starting the
live container, the rendered Caddyfile is validated with caddy validate in a throwaway
container.
Admin API
The Caddy admin API listens on 0.0.0.0:{{ management_port }} inside the container and is
published on the host at the same port (see management_port above). The admin API has no
built-in authentication, so management_port must only be reachable from trusted networks.
Restic backup and restore
Backups cover the whole {{ cs_rp_caddy_directory }} directory: the Caddyfile, sites/,
certs/, clients-ca.pem, logs/, and Caddy's own data//config/ storage, using the
restic_backup_restore_sftp
module. The {{ cs_rp_container_name }} container is stopped for the duration of the backup and
started again afterwards, even if the backup itself fails.
Restic repository credentials are generated per node/repo by the restic_setup_repositories
stage and read from Vault at
{{ cs_project_code }}/application-deployer/clusters/{{ cs_rp_restic_cluster_name }}/hosts/{{ cs_rp_restic_node_name }}/apps/restic/generated/sftp-repository-access-{{ cs_rp_restic_repo_name }}
(and the matching sftp-repository-password-... key). The repository name
(cs_rp_restic_repo_name, see Reverse Proxy for its default) must
be present in cs_restic_repos on the host running the restic repository.
Restore removes the {{ cs_rp_container_name }} container, restores the latest snapshot into a
temporary directory under /app/tmp (restores can exceed 50GB, too large for the default /tmp),
then rsyncs it into {{ cs_rp_caddy_directory }}. Restore does not recreate the container: run
reverse_proxy_install afterwards to bring it back up with the restored data.
Cleanup (dangerously_cleanup_reverse_proxy)
Destructive. Only runs when this exact tag is passed explicitly (it is intentionally excluded from the
plain reverse_proxy tag) and permanently deletes Caddy's own data on the host:
- Removes the
{{ cs_rp_container_name }}Docker container. - Deletes the UFW allow rules opened for ports 80/tcp, 443/tcp, 443/udp, and
management_port. - Deletes
{{ cs_rp_caddy_directory }}(theCaddyfile,sites/,certs/,clients-ca.pem,logs/, and Caddy's owndata//config/storage).
Tags
reverse_proxy: Deploy Caddy and run its Restic backup.reverse_proxy_install: Deploy Caddy only.reverse_proxy_restic_backup: Run Restic backup for Caddy.reverse_proxy_restic_restore: Restore Caddy's data from the latest Restic snapshot.dangerously_cleanup_reverse_proxy: Destructive. Removes the Caddy Docker container, closes its UFW ports, and deletes{{ cs_rp_caddy_directory }}. Never included by the plainreverse_proxytag.
Deployment
uv sync --all-extras --all-packages --no-progress
uv --offline run --no-sync --no-progress ansible-galaxy install -r requirements.yml
uv --offline run --no-sync --no-progress ansible-playbook playbook.yml --tags reverse_proxy_install \
--extra-vars "cs_rp_which_one=caddy"