server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Optional HTTP Basic Auth. entrypoint.sh rewrites this snippet on every # container start: when GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD are set # it enables auth_basic with a generated htpasswd, otherwise it leaves the # snippet empty (no auth, the default). Living at server level, the # directives inherit into every location below, including the /sidecar/ # proxy. Basic Auth sends credentials with each request, so pair it with # TLS (a reverse proxy in front) on anything but a trusted network. include /etc/nginx/geolibre-auth.conf; # Unauthenticated health endpoint for the container HEALTHCHECK (and any # external monitor). Must stay outside auth or enabling a password would # flip the container to unhealthy. location = /healthz { auth_basic off; access_log off; default_type text/plain; return 200 "ok\n"; } gzip on; gzip_vary on; gzip_proxied any; gzip_types text/css application/javascript application/json application/wasm image/svg+xml; gzip_min_length 1024; # Reverse-proxy the optional Python sidecar bundled in the same image. The # trailing slash strips the /sidecar/ prefix, so /sidecar/conversion/status # reaches the sidecar at /conversion/status. Being same-origin avoids CORS. location /sidecar/ { proxy_pass http://127.0.0.1:8765/; proxy_http_version 1.1; # The sidecar's TrustedHostMiddleware only accepts loopback hosts, so # forward a loopback Host rather than the public $host (which would be # rejected as a rebinding attempt). proxy_set_header Host 127.0.0.1; # Authenticate the proxy to the sidecar. entrypoint.sh substitutes the # per-container token here (and exports the same value to uvicorn). The # browser never holds this secret; nginx is the trusted caller. proxy_set_header X-GeoLibre-Token "__GEOLIBRE_SIDECAR_TOKEN__"; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 600s; } location / { try_files $uri /index.html; add_header Cache-Control "no-cache, must-revalidate" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Closely mirrors the Tauri desktop CSP (the browser build additionally # permits data: URIs in connect-src). 'unsafe-eval' is required by the # @google/earthengine and maplibre-gl-geoagent bundles; connect-src # stays broad because users connect to arbitrary tile/WMS endpoints. # The http://localhost:* / http://127.0.0.1:* (and ws:// equivalents) # allowances let users load PMTiles/COGs and other local test data from # a dev server on another port (e.g. http://localhost:8081) even though # the app itself is served from a different origin (issue #387). NOTE: # this Docker image is intended for local/single-user use. If it is ever # deployed to a shared or internet-facing host, the localhost allowances # let the served JS probe each visitor's own loopback (from the browser, # localhost is the visitor's machine, not the server) -- drop them if # you publish the image publicly. # The jsDelivr script-src allowances are path-scoped: /npm/ covers # maplibre-gl-vector's DuckDB-WASM + geojson-vt/vt-pbf (Add Vector # Layer) loaded via dynamic import()/importScripts(), and /pyodide/ # covers the Pyodide vector engine. # When adding or removing a *named* host from any CSP directive (e.g. a # collaboration server in connect-src, or a new CDN in script-src), # mirror the change in the Tauri CSP in # apps/geolibre-desktop/src-tauri/tauri.conf.json as well -- # wss://collab.geolibre.app is an example of a host duplicated in both. # (The browser build intentionally adds data: to connect-src; that # difference is documented in the "Closely mirrors" note above.) # The Tauri CSP additionally allows http://127.0.0.1:* / http://localhost:* # in frame-src/child-src so the desktop app can embed its locally # launched JupyterLab server in the Notebook panel. That is desktop-only # and intentionally NOT mirrored here: the web build embeds the # same-origin self-hosted JupyterLite site, already covered by 'self'. add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https: data: blob: http://127.0.0.1:* http://localhost:* wss://collab.geolibre.app ws://127.0.0.1:* ws://localhost:*; img-src 'self' data: blob: https:; media-src 'self' blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' blob: 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/ https://cdn.jsdelivr.net/pyodide/ https://accounts.google.com; child-src 'self' https://accounts.google.com https://www.google.com; frame-src 'self' https://accounts.google.com https://www.google.com; worker-src blob: 'self'" always; } # The service worker has a stable filename, so it must always revalidate; # otherwise the immutable asset rule below would pin an old worker for a year # and the autoUpdate flow could never pick up a redeploy. (Workbox's own # runtime chunk, workbox-.js, is content-hashed and safely immutable.) location = /sw.js { add_header Cache-Control "no-cache, must-revalidate" always; add_header X-Content-Type-Options "nosniff" always; } # nginx's default mime.types has no entry for .webmanifest; serve it with the # correct type and let it revalidate (it is small and changes on redeploys). location = /manifest.webmanifest { default_type application/manifest+json; add_header Cache-Control "no-cache" always; add_header X-Content-Type-Options "nosniff" always; } location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|ico|svg|webp|avif|wasm|woff2?|ttf|otf)$ { try_files $uri =404; add_header Cache-Control "public, max-age=31536000, immutable"; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; } }