rebours/nginx.conf
2026-02-24 15:13:37 +01:00

166 lines
8.4 KiB
Nginx Configuration File

# ─────────────────────────────────────────────────────────────────────────────
# 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";
}
}
}