diff --git a/birdnet-pipy/Dockerfile b/birdnet-pipy/Dockerfile index 5eaba9f98..e0436c58d 100644 --- a/birdnet-pipy/Dockerfile +++ b/birdnet-pipy/Dockerfile @@ -27,7 +27,7 @@ RUN mkdir -p /src \ WORKDIR /src/frontend RUN npm ci --prefer-offline -RUN npm run build +RUN npm run build -- --base=/birdnet/ FROM ${BUILD_FROM} @@ -57,7 +57,7 @@ RUN chmod 744 /ha_lsio.sh && if grep -qr "lsio" /etc; then /ha_lsio.sh "$CONFIGL # Copy local files COPY rootfs/ / RUN find /etc -type f \( -name "*.sh" -o -path "*/services.d/*/run" \) -exec chmod +x {} \; -COPY --from=frontend-builder /src/frontend/nginx.conf /etc/nginx/servers/nginx.conf +COPY rootfs/etc/nginx/servers/nginx.conf /etc/nginx/servers/nginx.conf # Uses /bin for compatibility purposes # hadolint ignore=DL4005 @@ -94,7 +94,7 @@ RUN sed -i \ COPY --from=frontend-builder /src/deployment/audio/scripts/start-icecast.sh /usr/local/bin/start-icecast.sh RUN chown icecast2 /usr/local/bin/start-icecast.sh && chmod 755 /usr/local/bin/start-icecast.sh -COPY --from=frontend-builder /src/frontend/dist /usr/share/nginx/html +COPY --from=frontend-builder /src/frontend/dist /usr/share/nginx/html/birdnet ################ # 4 Entrypoint # diff --git a/birdnet-pipy/rootfs/etc/nginx/includes/ingress_params.conf b/birdnet-pipy/rootfs/etc/nginx/includes/ingress_params.conf index ebb38131c..551eacdbc 100644 --- a/birdnet-pipy/rootfs/etc/nginx/includes/ingress_params.conf +++ b/birdnet-pipy/rootfs/etc/nginx/includes/ingress_params.conf @@ -1,7 +1,9 @@ absolute_redirect off; rewrite ^%%ingress_entry%%/(.*)$ /$1 break; sub_filter_once off; -sub_filter_types text/html; +sub_filter_types text/html text/css application/javascript; sub_filter '' ''; sub_filter 'href="/' 'href="%%ingress_entry%%/'; sub_filter 'src="/' 'src="%%ingress_entry%%/'; +sub_filter '"/birdnet/' '"%%ingress_entry%%/birdnet/'; +sub_filter 'url(/birdnet/' 'url(%%ingress_entry%%/birdnet/'; diff --git a/birdnet-pipy/rootfs/etc/nginx/servers/nginx.conf b/birdnet-pipy/rootfs/etc/nginx/servers/nginx.conf new file mode 100644 index 000000000..27d1ded03 --- /dev/null +++ b/birdnet-pipy/rootfs/etc/nginx/servers/nginx.conf @@ -0,0 +1,107 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + # Allow large file uploads (for database migration) + client_max_body_size 500M; + + # API proxy - forward /api/ requests to API server + # IMPORTANT: ^~ modifier prevents regex matches (like .png) from taking precedence + location ^~ /api/ { + proxy_pass http://api:5002; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Longer timeouts for migration imports + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # Internal auth verification endpoint (for nginx auth_request) + location = /internal/auth { + internal; + proxy_pass http://api:5002/api/auth/verify; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Cookie $http_cookie; + } + + # Auth error handler - returns JSON for API clients + location @stream_unauthorized { + default_type application/json; + return 401 '{"error": "Authentication required"}'; + } + + # Icecast audio stream proxy - forward /stream/ requests to Icecast server + # Protected by authentication when enabled + location ^~ /stream/ { + auth_request /internal/auth; + error_page 401 = @stream_unauthorized; + + proxy_pass http://icecast:8888/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Streaming-specific settings + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Handle static assets with long cache times + # Note: /api/ routes are handled above, so this only affects local static files + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location = / { + return 302 /birdnet/; + } + + location = /birdnet { + return 301 /birdnet/; + } + + # Handle Vue.js SPA routing - serve index.html for all routes that don't match static files + location /birdnet/ { + try_files $uri $uri/ /birdnet/index.html; + } + + # Socket.IO WebSocket proxy - forward /socket.io/ requests to API server + location /socket.io/ { + proxy_pass http://api:5002/socket.io/; + 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } + + # Error pages + error_page 404 /birdnet/index.html; + error_page 500 502 503 504 /birdnet/index.html; +}