server { listen 80; # The nginx image ships with gzip off, so the bundle went out raw: 555KB of # JS where gzip sends 167KB. Only text formats are listed — images, fonts # (woff2) and audio are already compressed and would only burn CPU. gzip on; gzip_vary on; gzip_min_length 1024; # The public URL sits behind another proxy, which adds a Via header. With # the default gzip_proxied off, nginx refuses to compress those responses. gzip_proxied any; # That same proxy speaks HTTP/1.0 upstream (nginx proxy_pass defaults to it), # and gzip_http_version defaults to 1.1 — so without this nothing here is # ever compressed, however the client asks. gzip_http_version 1.0; gzip_comp_level 6; gzip_types application/javascript application/json application/manifest+json image/svg+xml text/css text/plain; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } # A cached service worker or shell would pin the app to an old build, since # both are the files that point at every hashed asset. Assets themselves are # content-hashed by Vite, so they can be held forever. # nginx's stock mime.types has no .webmanifest entry, so it would otherwise # go out as application/octet-stream. location = /manifest.webmanifest { root /usr/share/nginx/html; default_type application/manifest+json; add_header Cache-Control "no-cache, must-revalidate"; } location = /sw.js { root /usr/share/nginx/html; add_header Cache-Control "no-cache, must-revalidate"; } location = /index.html { root /usr/share/nginx/html; add_header Cache-Control "no-cache, must-revalidate"; } location /assets/ { root /usr/share/nginx/html; add_header Cache-Control "public, max-age=31536000, immutable"; } # Admin endpoints need the admin key, not the regular API key. This prefix # location is longer than "/api", and nginx picks the longest matching # prefix location, so it wins for /api/admin/* while /api handles the rest. location /api/admin/ { proxy_pass http://backend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header Authorization "Bearer ${MUZICK_ADMIN_KEY}"; proxy_cache_bypass $http_upgrade; } # The cross-device event stream is long-lived and must arrive unbuffered: # with proxy buffering on, nginx holds each event until it has a chunk worth # forwarding, which is exactly the latency this channel exists to avoid. location /api/playback/stream { proxy_pass http://backend:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Authorization "Bearer ${MUZICK_API_KEY}"; proxy_set_header Connection ''; proxy_buffering off; proxy_cache off; proxy_read_timeout 1h; } location /api { proxy_pass http://backend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header Authorization "Bearer ${MUZICK_API_KEY}"; proxy_cache_bypass $http_upgrade; } }