# ───────────────────────────────────────────────────────────────────────────── # REBOUR — nginx.conf # Rôle : reverse proxy devant Elysia/Bun # SEO : pas de SSR → HTML statique pré-rendu, on optimise le delivery # (gzip, cache, headers) et la crawlabilité # ───────────────────────────────────────────────────────────────────────────── user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; use epoll; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr "$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ${request_time}s'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; server_tokens off; # ── Gzip : réduit poids HTML/CSS/JS → Core Web Vitals ─────────────────── gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 5; gzip_min_length 256; gzip_types text/plain text/css text/javascript text/xml application/javascript application/json application/xml image/svg+xml font/woff2; # ── Rate limiting ──────────────────────────────────────────────────────── limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m; limit_req_zone $binary_remote_addr zone=general:10m rate=60r/s; # ── Resolver DNS Docker ─────────────────────────────────────────────────── # 127.0.0.11 = resolver interne Docker. La résolution dynamique via variable # $backend évite le crash "host not found in upstream" au démarrage nginx # si le container "app" n'est pas encore prêt (upstream statique résout au # boot, pas à la requête — d'où le crash). resolver 127.0.0.11 valid=5s ipv6=off; # Variable résolue dynamiquement à chaque requête (pas au démarrage nginx) # Permet à nginx de démarrer même si "app" n'existe pas encore en DNS. map $host $elysia_backend { default "http://app:3000"; } # ── Redirection HTTP → HTTPS (décommenter en prod) ────────────────────── # server { # listen 80; # server_name rebour.studio www.rebour.studio; # return 301 https://rebour.studio$request_uri; # } server { listen 80; server_name _; # ── SSL / prod (décommenter avec Certbot) ──────────────────────────── # listen 443 ssl http2; # ssl_certificate /etc/letsencrypt/live/rebour.studio/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/rebour.studio/privkey.pem; # ssl_protocols TLSv1.2 TLSv1.3; # ssl_session_cache shared:SSL:10m; # ── Headers sécurité ──────────────────────────────────────────────── add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=()" always; # HSTS — décommenter en prod UNIQUEMENT après config SSL # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # ── Images / fonts : cache 1 an immutable ─────────────────────────── # Les crawlers sociaux (og:image) et Google téléchargent ces fichiers location ~* \.(jpg|jpeg|png|webp|svg|ico|woff2)$ { proxy_pass $elysia_backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Vary "Accept-Encoding"; } # ── CSS / JS : cache 1 an ──────────────────────────────────────────── location ~* \.(css|js)$ { proxy_pass $elysia_backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Vary "Accept-Encoding"; } # ── Checkout Stripe : rate-limit strict, no-cache ─────────────────── location = /api/checkout { limit_req zone=api burst=3 nodelay; proxy_pass $elysia_backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control "no-store"; } # ── Webhook Stripe : body raw, pas de buffering ────────────────────── location = /api/webhook { proxy_pass $elysia_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header stripe-signature $http_stripe_signature; proxy_request_buffering off; add_header Cache-Control "no-store"; } # ── API (session verify, etc.) ─────────────────────────────────────── location /api/ { limit_req zone=api burst=10 nodelay; proxy_pass $elysia_backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control "no-store"; } # ── SEO : sitemap + robots ─────────────────────────────────────────── location ~* ^/(sitemap\.xml|robots\.txt)$ { proxy_pass $elysia_backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control "public, max-age=86400"; } # ── Pages HTML ─────────────────────────────────────────────────────── # Cache 1h côté client, stale-while-revalidate pour UX fluide # Google re-crawle dès que le cache expire → pas de contenu périmé indexé location / { limit_req zone=general burst=20 nodelay; proxy_pass $elysia_backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; add_header Cache-Control "public, max-age=3600, stale-while-revalidate=86400"; } } }