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>
118 lines
4.5 KiB
Caddyfile
118 lines
4.5 KiB
Caddyfile
{
|
||
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
|
||
# (~10–30s) 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
|