# See web/nginx.conf.template (kept in sync) for why each directive is what it is. map $http_host $aonarr_host { "" $host; default $http_host; } # The Starr API below takes the API key as a query parameter (Overseerr, Bazarr and Homepage send it that way), and the default # access log would write it to the container log. This format logs the path only. log_format aonarr_starr '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent'; server { listen 9876; server_name _; root /usr/share/nginx/html; index index.html; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https: http:; img-src 'self' data: blob: https: http:; font-src 'self' data: https: http:; connect-src 'self'; worker-src 'self'; object-src 'none'; base-uri 'self'" always; # The Sonarr, Radarr, Lidarr and Readarr API AoNarr answers for Prowlarr, Overseerr, Bazarr, Homepage and # similar tools (server/src/starr): a tool's URL base is /sonarr, /radarr, /lidarr or /readarr. Only the # API and /ping go to the server; the app's own pages are never under these names. location ~ ^/(sonarr|radarr|lidarr|readarr)/(api|ping)(/|$) { access_log /var/log/nginx/access.log aonarr_starr; proxy_pass http://127.0.0.1:8989; proxy_set_header Host $aonarr_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; client_max_body_size 10m; proxy_connect_timeout 30s; proxy_send_timeout 300s; proxy_read_timeout 300s; } location /api/ { proxy_pass http://127.0.0.1:8989/api/; proxy_set_header Host $aonarr_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; client_max_body_size 10m; # A handful of endpoints (large-file recycle/import operations, bulk exports) can # legitimately run past nginx's 60s defaults — that's a slow-but-alive backend and should # surface as a real timeout (504), not the ambiguous "connection just dropped" a client # sees once the browser/any intermediate proxy gives up first. proxy_connect_timeout 30s; proxy_send_timeout 300s; proxy_read_timeout 300s; } # Backup restore accepts up to 1gb (routes/system.ts); /api/'s 10m cap answered a real # library's backup with nginx's own 413. Request buffering stays on so a slow upload is spooled # by nginx first instead of running into Node's 300s request timeout (408). auth_request checks # the caller is an admin before the body is read, so an anonymous upload is never spooled. location = /api/system/backup/restore { auth_request /__aonarr_admin_check; proxy_pass http://127.0.0.1:8989; proxy_set_header Host $aonarr_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; client_max_body_size 1g; proxy_connect_timeout 30s; proxy_send_timeout 600s; proxy_read_timeout 600s; } # The restore upload's admin check: /api/system is admin-only; headers are passed, the body isn't. location = /__aonarr_admin_check { internal; proxy_pass http://127.0.0.1:8989/api/system/resources; client_max_body_size 1g; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header Host $aonarr_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; } # Server-Sent Events (Activity queue push, System → Logs live tail): unbuffered so no event can # wait in an nginx buffer, with a read timeout far past the server's 30s heartbeat. location ~ ^/api/(activity/stream|system/logs/stream)$ { proxy_pass http://127.0.0.1:8989; proxy_set_header Host $aonarr_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_buffering off; proxy_cache off; proxy_connect_timeout 30s; proxy_read_timeout 3600s; } location / { try_files $uri /index.html; } # Vite's built JS/CSS filenames are content-hashed (index-.js) — a new build always gets a # new filename, so these are safe to cache forever. index.html is the opposite: it's the one file # that references those hashed names, so it must never be cached, or the browser can keep serving # a stale index.html pointing at JS that predates whatever update was just deployed — this was # the actual cause of "the UI still looks the same after updating the container." # `expires -1` sends the same Cache-Control: no-cache without an add_header here: any add_header # in a location replaces every server-level one, which would leave the page itself (every SPA # route lands here via try_files) without the Content-Security-Policy above. location = /index.html { expires -1; } # Browsers already re-check a registered service worker's script at most once every 24h by # spec regardless of headers — this just makes sure that check, whenever it happens, isn't # itself served from a stale HTTP cache. # Its add_header also keeps the page's CSP off the worker, whose own fetch() re-requests every # cross-origin poster and would be blocked by connect-src 'self'. location = /sw.js { add_header Cache-Control "no-cache"; } location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; } }