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>
This commit is contained in:
parent
8e41b09ac2
commit
e60c9d7057
6 changed files with 48 additions and 16 deletions
118
infrastructure/caddy/Caddyfile
Normal file
118
infrastructure/caddy/Caddyfile
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue