Skip to main content

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​

OptionTypeDescriptionDefault
cs_rp_caddy_clusterstringCluster name for the proxy{{ cs_cluster_name }}
cs_rp_caddy_directorystringDirectory for data and configuration/app/caddy
cs_rp_caddy_cluster_dnslist[string]DNS servers for the containerThe cluster's DNS servers (from Vault)
cs_rp_caddy_versionstringCaddy version2.11.7-alpine
cs_rp_caddy_docker_imagestringCaddy 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.

OptionTypeDescriptionApplied
domainslist[string]Domains for the applicationYes
forward_hoststringApplication hostname or IPYes
forward_portintApplication portYes
forward_schemestringhttp or httpsYes
hsts_enabledboolEnable HSTS (default true)Yes
hsts_subdomainsboolEnable HSTS for subdomains (default true)Yes
enabledboolEnable the application (default true)Yes
mtls_outboundboolRequire and verify client certificates (default true)Yes
ssl_forcedboolForce SSL (default true)Not applied: Caddy always auto-redirects HTTP to HTTPS for these sites
allow_websocket_upgradeboolAllow websocket upgrade (default false)Not applied: Caddy proxies WebSocket upgrades automatically
http2_supportboolEnable HTTP/2 support (default true)Not applied: HTTP/2 and HTTP/3 are always enabled
caching_enabledboolEnable caching (default true)Not applied: no equivalent
block_exploitsboolBlock 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/:

LogContentsFile
Application logCaddy's own startup, TLS, and admin API eventslogs/caddy.log
Per-site combined logEvery request handled by that site, success and error alikelogs/sites/<Application Name>/access.log
Per-site error logOnly 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 }} (the Caddyfile, sites/, certs/, clients-ca.pem, logs/, and Caddy's own data//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 plain reverse_proxy tag.

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"