server { listen 7777; server_name _; root /usr/share/nginx/html; index index.html; # Gzip compression gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/vnd.apple.mpegurl video/MP2T; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; # ── MinIO direct proxy (for presigned URL uploads/downloads) ── # MUST appear before the static-asset regex locations below, # otherwise image uploads (.jpg, .png, etc.) match the asset # caching rule first and return 405 on PUT. location ~ ^/(indeedhub-private|indeedhub-public)/ { resolver 127.0.0.11 valid=30s ipv6=off; set $minio_upstream http://minio:9000; proxy_pass $minio_upstream; proxy_http_version 1.1; # Pass the original Host so MinIO's signature verification matches # the host the presigned URL was generated for. proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Allow large file uploads (up to 5GB per chunk) client_max_body_size 5g; # No caching for upload responses add_header Cache-Control "no-store"; } # PWA Support - proper MIME types location ~* \.(?:manifest|webmanifest|json)$ { add_header Cache-Control "public, max-age=3600"; add_header Content-Type application/manifest+json; } # ── Service Worker — must be exact-match to avoid the immutable # caching regex below. Browsers require fresh SW scripts. location = /sw.js { add_header Cache-Control "no-cache, no-store, must-revalidate"; expires off; access_log off; } location ~* ^/workbox-.*\.js$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; expires off; access_log off; } # Static assets — long-lived immutable cache location ~* \.(?:js|css|woff2|woff|ttf|otf|eot|svg|png|jpg|jpeg|gif|ico)$ { expires 1y; add_header Cache-Control "public, immutable"; } # ── Backend API proxy ────────────────────────────────────── location /api/ { resolver 127.0.0.11 valid=30s ipv6=off; set $api_upstream http://api:4000; rewrite ^/api(.*) $1 break; proxy_pass $api_upstream; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Trust the outer reverse proxy's X-Forwarded-Proto when present, # otherwise fall back to the connection scheme. proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; # Preserve the original /api prefix so NIP-98 URL verification # can reconstruct the URL the client actually signed. proxy_set_header X-Forwarded-Prefix /api; proxy_read_timeout 300s; proxy_send_timeout 300s; # Handle large video uploads (up to 5GB) client_max_body_size 5g; } # ── MinIO storage proxy (public bucket) ──────────────────── # Serves poster images, HLS segments, etc. with caching # ^~ ensures this prefix takes priority over the static-asset # regex that would otherwise intercept .jpg/.png requests. location ^~ /storage/ { resolver 127.0.0.11 valid=30s ipv6=off; set $minio_upstream http://minio:9000; rewrite ^/storage/(.*) /indeedhub-public/$1 break; proxy_pass $minio_upstream; proxy_http_version 1.1; proxy_set_header Host minio:9000; # Cache static assets aggressively proxy_cache_valid 200 1d; proxy_cache_valid 404 1m; expires 1d; add_header Cache-Control "public, max-age=86400"; add_header X-Cache-Status $upstream_cache_status; } # ── MinIO storage proxy (private bucket -- for HLS key delivery) ─ location ^~ /storage-private/ { resolver 127.0.0.11 valid=30s ipv6=off; set $minio_upstream http://minio:9000; rewrite ^/storage-private/(.*) /indeedhub-private/$1 break; proxy_pass $minio_upstream; proxy_http_version 1.1; proxy_set_header Host minio:9000; # Do NOT cache private content add_header Cache-Control "no-store"; } # ── WebSocket proxy to Nostr relay (Docker service) ──────── location /relay { resolver 127.0.0.11 valid=30s ipv6=off; set $relay_upstream http://relay:8080; rewrite ^/relay(.*) /$1 break; proxy_pass $relay_upstream; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # ── Vue Router - SPA fallback ────────────────────────────── location / { try_files $uri $uri/ /index.html; } # Health check endpoint location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } }