trails/infrastructure/caddy/Caddyfile
Ullrich Schäfer e60c9d7057 refactor(infra): mount config dirs not single files; reload on deploy
Single-file bind mounts (./foo.yml:/etc/foo.yml) pin to the host file's
inode at container-create time. The CD pipeline scp's a replacement file
(new inode), so the running container keeps reading the OLD inode —
`docker compose up -d` won't recreate on a content-only change, and
neither restart/SIGHUP/`caddy reload` re-reads the new file. Net effect:
config-only infra PRs deployed "successfully" but never took effect
(confirmed with PR #500's prometheus.yml; the WAL retention work only
applied because #498 also changed docker-compose.yml, forcing a
recreate). Caddyfile changes had the same latent gap.

Switch the four single-file config mounts to DIRECTORY mounts, which
resolve children live so a reload/restart picks up the new file:

- prometheus, loki, promtail: mount ./<svc> dir (loki/promtail
  --config.file paths updated to the real filenames).
- caddy: move Caddyfile into caddy/ and mount the dir; ./sites overlays
  /etc/caddy/sites (caddy/sites/.gitkeep keeps the mountpoint). Container
  path /etc/caddy/Caddyfile is unchanged, so every `caddy reload --config
  /etc/caddy/Caddyfile` ref still works. scp source paths in cd-infra and
  cd-apps updated to ship the caddy/ dir.

cd-infra now applies config-only changes after `up -d`: SIGHUP prometheus
(zero downtime), restart loki+promtail (no SIGHUP reload), caddy reload
(graceful). Mirrored prometheus/loki mounts in docker-compose.dev.yml.

Validated: docker compose config (both files), caddy validate from the
new path, read-only-parent + sites-overlay mount mechanics, workflow YAML.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:10:35 +02:00

118 lines
4.5 KiB
Caddyfile
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
metrics
admin 0.0.0.0:2019
}
(security_headers) {
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"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' wss: https://*.sentry.io https://*.ingest.de.sentry.io; font-src 'self';"
}
}
(block_scanners) {
@scanners path /.env* /.git* /wp-* /wp-admin* /admin* /config.* /backup* /.aws* /.docker* /composer* /vendor* *.php *.asp
respond @scanners 403
}
{$DOMAIN:trails.cool} {
import security_headers
import block_scanners
log {
output stdout
format json
}
reverse_proxy journal:3000 {
# During an `apps` deploy the journal container is briefly down
# (~1030s) while compose swaps containers. Without these,
# Caddy returns 502 immediately and the `caddy-502-rate` alert
# trips on every deploy. With them, Caddy holds and retries
# against the upstream for up to 30s — restart becomes
# invisible to clients. A real outage longer than 30s still
# 502s and correctly trips the alert.
lb_try_duration 30s
lb_try_interval 250ms
}
}
www.{$DOMAIN:trails.cool} {
redir https://{$DOMAIN:trails.cool}{uri} permanent
}
grafana.internal.{$DOMAIN:trails.cool} {
reverse_proxy grafana:3000
}
planner.{$DOMAIN:trails.cool} {
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"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' wss: https://{$DOMAIN:trails.cool} https://*.sentry.io https://*.ingest.de.sentry.io; font-src 'self';"
}
import block_scanners
log {
output stdout
format json
}
reverse_proxy planner:3001 {
# Same rationale as the journal block — see the comment there.
lb_try_duration 30s
lb_try_interval 250ms
}
}
# ── Staging ──────────────────────────────────────────────────────────────
# Persistent staging instance. CSP allow-lists hardcode `staging.trails.cool`
# rather than `{$DOMAIN}` because the Caddy container runs with the
# production DOMAIN env (`trails.cool`); staging blocks need their own domain
# baked in. Upstreams are reached via `host.docker.internal` because the
# staging compose project publishes its containers on the host's loopback
# (127.0.0.1:3100/3101) rather than joining the production Caddy network.
staging.{$DOMAIN:trails.cool} {
import security_headers
import block_scanners
log {
output stdout
format json
}
reverse_proxy host.docker.internal:3110 {
lb_try_duration 30s
lb_try_interval 250ms
}
}
planner.staging.{$DOMAIN:trails.cool} {
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"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
# connect-src includes wss + https://*.staging so PR-preview journals
# (pr-N.staging.trails.cool) can use this shared planner. PR previews
# are journal-only; this is the planner they all talk to.
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' wss://*.staging.{$DOMAIN:trails.cool} https://*.staging.{$DOMAIN:trails.cool} https://staging.{$DOMAIN:trails.cool} https://*.sentry.io https://*.ingest.de.sentry.io; font-src 'self';"
}
import block_scanners
log {
output stdout
format json
}
reverse_proxy host.docker.internal:3111 {
lb_try_duration 30s
lb_try_interval 250ms
}
}
# Per-PR preview snippets are written by cd-staging.yml into
# /etc/caddy/sites/pr-<N>.caddyfile and picked up here on Caddy reload. The
# glob is allowed to match nothing — Caddy treats an empty match as a no-op.
import /etc/caddy/sites/*.caddyfile