Skip to content

Nginx configuration

The frontend uses Nginx as a reverse proxy and static file server. The configuration lives in frontend/nginx.conf.template.

Architecture

flowchart LR
    Browser --> Nginx
    Nginx -->|"/api/*"| Backend["Backend :443"]
    Nginx -->|"static files"| Static["Static files"]

Nginx serves two purposes: static file server for the Svelte frontend build, and reverse proxy for API requests to the backend.

Configuration breakdown

Server block

server {
    listen 5001;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;
Directive Purpose
listen 5001 Internal container port (mapped via Docker Compose)
server_name _ Catch-all server name
root Static files from Svelte build

Compression

    # Enable 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/x-font-ttf font/opentype image/svg+xml image/x-icon;
    gzip_disable "msie6";

Gzip compression reduces bandwidth for text-based assets. Binary files (images, fonts) are excluded as they're already compressed.

API proxy

    location /api/ {
        proxy_pass ${BACKEND_URL};
        proxy_ssl_verify off;
        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;

        # Forward cookies
        proxy_pass_request_headers on;
        proxy_set_header Cookie $http_cookie;
Directive Purpose
proxy_pass Forward to backend service over HTTPS
proxy_ssl_verify off Skip certificate verification (internal traffic)
X-Real-IP Pass client IP to backend for rate limiting
X-Forwarded-Proto Preserve original protocol for redirect URLs
Cookie Forward authentication cookies

SSE (Server-Sent Events)

SSE endpoints require special handling to prevent buffering:

        # SSE specific settings for /api/v1/events/
        location ~ ^/api/v1/events/ {
            proxy_pass ${BACKEND_URL};
            proxy_ssl_verify off;
            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;

            # Forward cookies
            proxy_pass_request_headers on;
            proxy_set_header Cookie $http_cookie;

            # SSE configuration
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            proxy_buffering off;
            proxy_cache off;
            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;

            # Disable buffering for SSE
            proxy_set_header X-Accel-Buffering no;

            # CORS headers for SSE
            add_header Cache-Control 'no-cache';
            add_header X-Accel-Buffering 'no';
        }
Directive Purpose
Connection '' Disable connection header for HTTP/1.1 keep-alive
proxy_http_version 1.1 Required for chunked transfer encoding
proxy_buffering off Stream responses immediately
proxy_read_timeout 86400s 24-hour timeout for long-lived connections
X-Accel-Buffering no Disable upstream buffering

Without these settings, SSE events would be buffered and delivered in batches instead of real-time.

Static asset caching

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header X-Content-Type-Options "nosniff";
    }

    # Cache build directory assets with long expiry
    location /build/ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header X-Content-Type-Options "nosniff";
    }

    # HTML files should not be cached
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
    }

Svelte build outputs hashed filenames (app.abc123.js), making them safe to cache indefinitely. HTML files must never be cached to ensure users get the latest asset references.

Security headers

    location / {
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; connect-src 'self';";
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-Content-Type-Options "nosniff";
        add_header Referrer-Policy "strict-origin-when-cross-origin";
        add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
        try_files $uri $uri/ /index.html;
    }

Content Security Policy

Directive Value Purpose
default-src 'self' Fallback for unspecified directives
script-src 'self' 'unsafe-inline' Allow inline scripts (Svelte)
style-src 'self' 'unsafe-inline' Allow inline styles (Svelte)
img-src 'self' data: blob: Allow data: URLs for SVG icons
font-src 'self' data: Allow embedded fonts
object-src 'none' Block plugins (Flash, Java)
frame-ancestors 'none' Prevent clickjacking
connect-src 'self' XHR/fetch/WebSocket same-origin

The data: source is required for the Monaco editor's inline SVG icons.

Other security headers

Header Value Purpose
X-Frame-Options SAMEORIGIN Legacy clickjacking protection
X-Content-Type-Options nosniff Prevent MIME sniffing
Referrer-Policy strict-origin-when-cross-origin Limit referrer leakage
Permissions-Policy Deny geolocation, mic, camera Disable unused APIs

SPA routing

The try_files $uri $uri/ /index.html directive enables client-side routing. When a URL like /editor is requested directly, Nginx serves index.html and lets the Svelte router handle the path.

Deployment

The nginx configuration uses environment variable substitution via the official nginx Docker image's built-in envsubst feature:

# Production stage
FROM nginx:alpine

# Copy built static files
COPY --from=builder /app/public /usr/share/nginx/html

# Copy nginx config template (envsubst runs at container startup)
# The nginx image automatically processes /etc/nginx/templates/*.template
# and outputs to /etc/nginx/conf.d/ with the .template suffix removed
COPY nginx.conf.template /etc/nginx/templates/default.conf.template

The nginx image automatically processes files in /etc/nginx/templates/*.template and outputs the result to /etc/nginx/conf.d/ with the .template suffix removed.

Environment variables

Variable Purpose Example
BACKEND_URL Backend service URL for API proxy https://backend:443

Set this via the docker-compose environment section.

Rebuilding

To apply nginx configuration changes:

docker compose build --no-cache frontend
docker compose restart frontend

Troubleshooting

Issue Cause Solution
SSE connections dropping Default 60s proxy_read_timeout Verify 86400s timeout is set
CSP blocking resources Missing source in directive Check browser console, add blocked source
502 Bad Gateway Backend unreachable docker compose logs backend
Assets not updating Browser cache Clear cache or verify no-cache on HTML