map $http_origin $cors_origin { default ""; "https://local.example.com:5173" $http_origin; "https://app.example.com" $http_origin; } # Garage S3 web endpoint (port 3902 for public reads via website mode) upstream garage_web { server 10.10.10.39:3902; server 10.10.10.40:3902 backup; server 10.10.10.41:3902 backup; } server { listen 5500; server_name _; # Use Docker's embedded DNS for dynamic container resolution # Prevents stale IPs when containers restart and get new addresses resolver 127.0.0.11 valid=10s; set $upstream_web web:8000; # 🔒 SECURITY: Remote Oathkeeper proxies here with auth headers already added # Flow: Internet → Oathkeeper → This Nginx → Django/S3 # Internal auth check endpoint for S3 media # Forwards all Oathkeeper headers so Django middleware can authenticate location = /internal-auth-check { internal; proxy_pass http://$upstream_web/api/media-auth/; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; # Forward standard headers proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 🔒 Forward all Oathkeeper auth headers for middleware validation proxy_set_header X-Oathkeeper-Secret $http_x_oathkeeper_secret; proxy_set_header X-User-Id $http_x_user_id; proxy_set_header X-User-Email $http_x_user_email; proxy_set_header X-User-First-Name $http_x_user_first_name; proxy_set_header X-User-Last-Name $http_x_user_last_name; proxy_set_header X-User-Phone $http_x_user_phone; proxy_set_header X-User-Profile-Type $http_x_user_profile_type; proxy_set_header X-Django-Profile-Id $http_x_django_profile_id; } # S3-backed media serving with auth_request # Flow: auth_request → Django validates → proxy to Garage S3 location /api/media/ { # Auth check before proxying to S3 auth_request /internal-auth-check; # Strip /api/media/ prefix and proxy to Garage web endpoint # Website mode serves bucket content at root path rewrite ^/api/media/(.*)$ /$1 break; proxy_pass http://garage_web/; proxy_http_version 1.1; # Set Host header to bucket name for Garage website mode proxy_set_header Host nexus-media.web.garage.nebula; # Video streaming support - forward range requests proxy_set_header Range $http_range; proxy_set_header If-Range $http_if_range; add_header Accept-Ranges bytes; # Cache headers for static content (private = browser only, not CDN) add_header Cache-Control "private, max-age=3600"; } # Legacy internal location (kept for backwards compatibility during migration) # Can be removed once S3 migration is verified location /media-internal/ { internal; alias /app/media/; add_header Accept-Ranges bytes; sendfile on; sendfile_max_chunk 1m; tcp_nopush on; tcp_nodelay on; } # All other requests proxy to Django # Oathkeeper has already validated session and added headers before reaching here location / { proxy_pass http://$upstream_web; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; # WebSocket support (for GraphQL subscriptions) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # Large file uploads (videos) - increased from 50m client_max_body_size 250m; proxy_buffering off; proxy_request_buffering off; proxy_read_timeout 600s; proxy_send_timeout 600s; } }