Self-hosted instances no longer inherit the trails.cool flagship Sentry DSNs. Code paths now read DSN strictly from env — unset = no Sentry init, no events sent. Flagship continues to report unchanged: cd-apps.yml and cd-staging.yml inject the public DSNs as workflow env vars, which feed into: - runtime via \`infrastructure/app.env\` / \`staging.env\` (\`SENTRY_DSN_JOURNAL\` / \`SENTRY_DSN_PLANNER\` → compose env per service) - build time via docker build-arg \`VITE_SENTRY_DSN\` (journal only; planner has no client Sentry init). Sentry DSNs are public-by-design (transmitted unencrypted from the client JS bundle), so embedding them as plaintext workflow env vars is no worse than the runtime exposure. Forks should replace these with their own DSNs or remove the workflow env lines to ship Sentry-free. Files changed: - apps/journal/server.ts: \`process.env.SENTRY_DSN\` only, no fallback - apps/planner/server.ts: same - apps/journal/app/lib/sentry.client.ts: \`import.meta.env.VITE_SENTRY_DSN\` only; falsy = skip init - apps/journal/Dockerfile: new \`ARG VITE_SENTRY_DSN\` baked into build - infrastructure/docker-compose.yml: pass \`SENTRY_DSN\` per service - infrastructure/docker-compose.staging.yml: same - .github/workflows/cd-apps.yml: workflow env + build-arg + app.env echo - .github/workflows/cd-staging.yml: same Full repo: pnpm typecheck, pnpm lint, pnpm test, journal build all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
4.4 KiB
YAML
108 lines
4.4 KiB
YAML
# Staging / PR-preview compose file.
|
|
#
|
|
# Used for both the persistent staging stack and ephemeral PR previews.
|
|
# The cd-staging.yml workflow picks the project name and fills in the host
|
|
# ports + DOMAIN + STAGING_DATABASE from the PR number (or "staging" for the
|
|
# persistent stack):
|
|
#
|
|
# # Persistent staging
|
|
# PROJECT=trails-staging
|
|
# JOURNAL_HOST_PORT=3100 PLANNER_HOST_PORT=3101 \
|
|
# DOMAIN=staging.trails.cool STAGING_DATABASE=trails_staging \
|
|
# docker compose -f docker-compose.staging.yml -p $PROJECT up -d
|
|
#
|
|
# # PR 123 preview (port = 3200 + 2N for journal, 3201 + 2N for planner)
|
|
# PROJECT=trails-pr-123
|
|
# JOURNAL_HOST_PORT=3446 PLANNER_HOST_PORT=3447 \
|
|
# DOMAIN=pr-123.staging.trails.cool STAGING_DATABASE=trails_pr_123 \
|
|
# JOURNAL_IMAGE_TAG=pr-123 PLANNER_IMAGE_TAG=pr-123 \
|
|
# docker compose -f docker-compose.staging.yml -p $PROJECT up -d
|
|
#
|
|
# `trails-shared` is created by the production compose file (see
|
|
# docker-compose.yml) so staging/preview containers can reach the production
|
|
# `postgres` host. BRouter lives on a separate host and is reached over
|
|
# vSwitch using the production-shared BROUTER_URL / BROUTER_AUTH_TOKEN.
|
|
#
|
|
# Container names are not pinned — Docker Compose generates them from the
|
|
# project name (e.g. `trails-pr-123-journal-1`), which keeps each preview
|
|
# isolated without manual naming.
|
|
|
|
services:
|
|
journal:
|
|
image: ghcr.io/trails-cool/journal:${JOURNAL_IMAGE_TAG:-latest}
|
|
restart: unless-stopped
|
|
# Published on 0.0.0.0 (not 127.0.0.1) so the production Caddy container
|
|
# can reach it via host.docker.internal — Docker's host-gateway resolves
|
|
# to the bridge IP, not loopback. The Hetzner Cloud firewall blocks all
|
|
# inbound on ports 3000+ from the public internet, so this is effectively
|
|
# internal-only despite the bind address.
|
|
ports:
|
|
- "${JOURNAL_HOST_PORT:?JOURNAL_HOST_PORT must be set}:3000"
|
|
networks:
|
|
- trails-shared
|
|
environment:
|
|
DOMAIN: ${DOMAIN:?DOMAIN must be set}
|
|
ORIGIN: https://${DOMAIN}
|
|
# PR previews override this to point at the persistent staging
|
|
# planner (planner.staging.trails.cool). Persistent staging defaults
|
|
# to its own planner subdomain.
|
|
PLANNER_URL: ${PLANNER_URL:-https://planner.${DOMAIN}}
|
|
IS_FLAGSHIP: ""
|
|
DATABASE_URL: postgres://trails:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in SOPS secrets.app.env}@postgres:5432/${STAGING_DATABASE:?STAGING_DATABASE must be set}
|
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}
|
|
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
|
|
NODE_ENV: production
|
|
PORT: 3000
|
|
SENTRY_DSN: ${SENTRY_DSN_JOURNAL:-}
|
|
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
|
|
SMTP_URL: ""
|
|
SMTP_FROM: trails.cool staging <noreply@staging.trails.cool>
|
|
WAHOO_CLIENT_ID: ""
|
|
WAHOO_CLIENT_SECRET: ""
|
|
WAHOO_WEBHOOK_TOKEN: ""
|
|
DEMO_BOT_ENABLED: ""
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 256M
|
|
|
|
planner:
|
|
image: ghcr.io/trails-cool/planner:${PLANNER_IMAGE_TAG:-latest}
|
|
# Only the persistent staging stack runs a planner. PR previews are
|
|
# journal-only and point their PLANNER_URL at the persistent
|
|
# planner.staging.trails.cool. Saves ~256MB per active preview.
|
|
profiles: ["persistent"]
|
|
restart: unless-stopped
|
|
# Same rationale as the journal port — see the comment there.
|
|
ports:
|
|
- "${PLANNER_HOST_PORT:-3101}:3001"
|
|
networks:
|
|
- trails-shared
|
|
environment:
|
|
BROUTER_URL: ${BROUTER_URL:?BROUTER_URL must be set}
|
|
BROUTER_AUTH_TOKEN: ${BROUTER_AUTH_TOKEN:?BROUTER_AUTH_TOKEN must be set}
|
|
OVERPASS_URLS: ${OVERPASS_URLS:-https://lz4.overpass-api.de/api/interpreter,https://overpass-api.de/api/interpreter}
|
|
DATABASE_URL: postgres://trails:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in SOPS secrets.app.env}@postgres:5432/${STAGING_DATABASE}
|
|
NODE_ENV: production
|
|
PORT: 3001
|
|
SENTRY_DSN: ${SENTRY_DSN_PLANNER:-}
|
|
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -sf http://localhost:3001/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 256M
|
|
|
|
networks:
|
|
trails-shared:
|
|
external: true
|
|
name: trails-shared
|