§7 — Vue de déploiement¶
Infrastructure, pipeline, et conventions ops. Les trois post-mortems (voir sous-section dédiée) documentent les incidents rencontrés et les garde-fous résultants.
7.1 Topologie production¶
graph TB
subgraph internet [Internet]
User[Public web]
Recruiter[Relecteur technique]
Github[GitHub App
push main]
end
subgraph dns [DNS OVH]
DNS[A records
alpimonitor.fr
api.alpimonitor.fr
storybook.alpimonitor.fr
docs.alpimonitor.fr]
end
subgraph vps [VPS Hetzner — 95.216.196.69]
Traefik[Traefik
TLS Lets Encrypt
reverse proxy]
Coolify[Coolify v4
orchestrator]
subgraph prod [alpimonitor-prod]
Web[nginx:1.27-alpine
SPA static
6 headers sécurité]
API[node:20-alpine
Fastify + cron ingestion]
DB[(postgres:16-alpine
alpimonitor-pgdata)]
end
subgraph sb [alpimonitor-storybook]
SB[nginx:1.27-alpine
Storybook static]
end
end
subgraph external [External]
LetsEncrypt[Lets Encrypt
ACME HTTP-01]
LINDAS[LINDAS SPARQL
OFEV/BAFU]
OSM[OpenStreetMap tiles]
end
User & Recruiter --> DNS
DNS --> Traefik
Traefik -->|alpimonitor.fr| Web
Traefik -->|api.alpimonitor.fr| API
Traefik -->|storybook.alpimonitor.fr| SB
API <-->|Prisma| DB
API -->|cron 10 min SPARQL| LINDAS
Web -->|tiles HTTPS| OSM
Traefik <-->|ACME challenge| LetsEncrypt
Github -->|webhook| Coolify
Coolify -->|build + deploy| Web
Coolify -->|build + deploy| API
Coolify -->|build + deploy| SB
Quatre sous-domaines servis par une seule instance Traefik gérée par Coolify. Deux « ressources » Coolify distinctes : alpimonitor-prod (web + api + postgres, orchestrés par docker-compose.prod.yml) et alpimonitor-storybook (container statique indépendant). La ressource docs.alpimonitor.fr sera ajoutée en Phase 4 de cette documentation.
7.2 Fichiers impliqués¶
apps/api/Dockerfile— image runtime multi-stage.node:20-alpinebase, user non-rootapp, tini PID 1,prisma migrate deployau démarrage viaentrypoint.sh. Pre-création + chown de/app/var/lindas-archiveavantUSER app(cf. post-mortem EACCES).apps/api/entrypoint.sh— orchestreprisma migrate deploy→prisma db seed(conditionnelSEED_ON_BOOT) →exec node dist/index.js.set -eu,PATH="/app/node_modules/.bin:$PATH"pourtsx.apps/web/Dockerfile— build Vite (stagebuild) puis service statique vianginx:1.27-alpine(stageruntime). SPA fallback, cache assets hashés immutable, gzip actif.apps/web/nginx.conf— vhost principal, includenginx-security-headers.conf.apps/web/nginx-security-headers.conf— 6 headers HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Cross-Origin-Opener-Policy.apps/web/Dockerfile.storybook+apps/web/nginx-storybook.conf+apps/web/nginx-storybook-security-headers.conf— variantes pour le sous-domaine Storybook, CSP assouplie (frame-ancestors 'self'+script-src 'unsafe-eval'— cf. ADR-009 §Déploiement).docker-compose.prod.yml— regroupepostgres+api+weben une seule ressource Coolify. Pas de réseau custom (cf. post-mortem Traefik) — Compose crée<project>_defaultauto-utilisé par Traefik..env.production.example— variables à renseigner dans le panneau Coolify :DATABASE_URL,CORS_ORIGINS,VITE_API_BASE_URL,POSTGRES_*,SEED_ON_BOOT,INGESTION_*.
7.3 Pipeline de déploiement¶
- Développeur push sur la branche
main(ou merge d'une PR). - GitHub App
sodigitaljeremyémet un webhook HTTPS vers Coolify. - Coolify clone le repo sur le VPS, exécute
docker compose buildavec les Dockerfiles correspondants. - Au succès, Coolify stoppe les anciens containers, démarre les nouveaux, et swap Traefik vers les nouvelles instances (zero-downtime swap la plupart du temps).
/api/v1/healthet le static/servis 200 ⇒ le deploy est considéré comme OK.
Aucune gate CI-pre-merge aujourd'hui — la CI GitHub Actions tourne informativement sur push et PR, mais le merge sur main n'est pas bloqué par un échec CI. C'est une dette assumée pour la démo ; la CI reste une référence factuelle.
Rollback : via le panneau Coolify, re-deploy de l'image précédente. Alternative git : git revert <sha> + push.
7.4 Sous-domaines¶
alpimonitor.fr— SPA Vue (nginx static) + redirectionwww.alpimonitor.fr. TLS Let's Encrypt géré par Traefik.api.alpimonitor.fr— API Fastify. CORS allowlistalpimonitor.fr+www.alpimonitor.fr+ origines dev (envCORS_ORIGINS).storybook.alpimonitor.fr— catalogue design system statique (cf. ADR-009).docs.alpimonitor.fr— documentation arc42 (ce site). Phase 4 de la roadmap docs — Dockerfile + nginx vhost à créer.
DNS OVH : 4 records A pointant vers 95.216.196.69. Propagés 2026-04-20, TTL standard.
7.5 Stratégie sécurité¶
- 6 headers nginx servis par le vhost SPA : HSTS (
max-age=31536000; includeSubDomains), CSP, X-Frame-OptionsDENY, X-Content-Type-Optionsnosniff, Referrer-Policystrict-origin-when-cross-origin, Cross-Origin-Opener-Policysame-origin. Audit axe-core informel + Puppeteer smoke validés (§10 audit). - CORS allowlist côté API — aucune étoile, chaque origine whitelistée explicitement.
- Container non-root — user
app(uid/gid 1001) dans les images API et web. Volume/app/varpre-créé + chowné dans le Dockerfile (lesson post-mortem EACCES). - Secrets via env uniquement —
.env.productionn'est jamais commité, les valeurs vivent dans le panneau Coolify..env.production.exampledocumente la liste. - Zod systématique — validation runtime sur tous les endpoints. Un payload malformé → 400 avec
VALIDATION_ERROR, pas de crash serveur. - Non-wired en v1 (reportés prod réelle) : Helmet (headers côté API),
@fastify/rate-limit, auth JWT + bcrypt. Read-only public acceptable pour démo.
7.6 Observabilité¶
- Logs — Pino JSON stdout, captés par Coolify. Agrégation Loki/Datadog reportée v2.
- Healthcheck —
/api/v1/healthconsommé par Coolify + Traefik. Retourne 503 si DB down. - Status —
/api/v1/statusexposeIngestionRun.lastRun,lastSuccessAt, compteurs journée. Badge UIMStatusBadgedansOHeroSectionfait le polling 60 s. - Pas d'APM, pas de tracing distribué — hors scope v1.