events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; resolver 127.0.0.11 ipv6=off valid=30s; map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name www.linearvoid.tv; return 301 https://linearvoid.tv$request_uri; } server { listen 80 default_server; server_name linearvoid.tv localhost 127.0.0.1; # Resumable admin upload chunks are 8MB by default; allow safe headroom. client_max_body_size 64m; root /srv/frontend; index index.html; location = /admin { return 301 /admin/; } location = /admin/login { return 301 /admin/login/; } location = /_admin_auth { internal; set $chat_upstream http://chat:3000; proxy_pass $chat_upstream/api/admin/session; proxy_http_version 1.1; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header Cookie $http_cookie; 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; } location /admin/login/ { try_files $uri $uri/ /admin/login/index.html; } location /admin/ { auth_request /_admin_auth; error_page 401 = @admin_login_redirect; try_files $uri $uri/ /admin/index.html; } location /assets/ { try_files $uri =404; } # Serve static frontend location / { try_files $uri $uri/ /index.html; } # Proxy chat WebSocket location /ws { set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; 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; } location = /api/admin/login { set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; 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_set_header Cookie $http_cookie; } location = /api/admin/logout { set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; 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_set_header Cookie $http_cookie; } location = /api/admin/session { set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; 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_set_header Cookie $http_cookie; } location /api/admin/ { auth_request /_admin_auth; error_page 401 = @admin_api_unauthorized; set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; 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_set_header Cookie $http_cookie; } location /api/ { set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; 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; } location = /metrics { set $chat_upstream http://chat:3000; proxy_pass $chat_upstream; proxy_http_version 1.1; 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; } location @admin_api_unauthorized { add_header Content-Type application/json; return 401 '{"error":"unauthorized"}'; } location @admin_login_redirect { return 302 /admin/login/; } location = /hls/playlist.m3u8 { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "GET, OPTIONS"; add_header Access-Control-Max-Age 86400; return 204; } alias /srv/hls/playlist.m3u8; add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0"; add_header Access-Control-Allow-Origin "*"; } location = /hls/init.mp4 { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "GET, OPTIONS"; add_header Access-Control-Max-Age 86400; return 204; } alias /srv/hls/init.mp4; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Access-Control-Allow-Origin "*"; } location ~* ^/hls/(.+\.(ts|m4s))$ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "GET, OPTIONS"; add_header Access-Control-Max-Age 86400; return 204; } alias /srv/hls/$1; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Access-Control-Allow-Origin "*"; } # Local/dev fallback HLS origin. Production media should use MEDIA_DOMAIN. location /hls/ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "GET, OPTIONS"; add_header Access-Control-Max-Age 86400; return 204; } alias /srv/hls/; add_header Cache-Control "public, max-age=60"; add_header Access-Control-Allow-Origin "*"; } # Health checks — aggregated from upstream services location /health { default_type application/json; return 200 '{"status":"ok","service":"nginx","timestamp":"$time_iso8601"}'; } location /nginx-status { stub_status on; allow 127.0.0.1; allow 172.16.0.0/12; allow 192.168.0.0/16; deny all; } } }