# Rendered by the nginx image's envsubst from ${DOMAIN}. TLS uses a Cloudflare Origin # Certificate (mode "Full (strict)"): Cloudflare terminates the browser-facing HTTPS and # connects to this origin over HTTPS validated against its Origin CA. nginx serves the SPA # and reverse-proxies the API to gunicorn. upstream django { server backend:8000; } # ── HTTP: redirect to HTTPS (Cloudflare normally handles this at the edge) ──── server { listen 80; server_name ${DOMAIN}; return 301 https://$host$request_uri; } # ── HTTPS: the app (Cloudflare Origin Certificate) ─────────────────────────── server { listen 443 ssl; http2 on; server_name ${DOMAIN}; ssl_certificate /etc/nginx/certs/origin.pem; ssl_certificate_key /etc/nginx/certs/origin.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; client_max_body_size 10M; root /usr/share/nginx/html; index index.html; # Long-cache hashed Vite assets. location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; } # Django collected static + media (shared volumes). location /static/ { alias /app/staticfiles/; expires 30d; access_log off; } location /media/ { alias /app/media/; expires 30d; } # API + admin -> gunicorn. Cloudflare connects over HTTPS, so $scheme is https. location /api/ { proxy_pass http://django; 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; proxy_redirect off; } location /admin/ { proxy_pass http://django; 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; proxy_redirect off; } # SPA client-side routing fallback. index.html must NOT be cached by the browser, or # a deploy's new hashed-asset references won't be picked up (the /assets/ files are # content-hashed, so they keep the long immutable cache above). location / { add_header Cache-Control "no-cache"; try_files $uri $uri/ /index.html; } }