# Hardened nginx config for a static portfolio # Runs as user 'nginx' (non-root), listens on port 8080 worker_processes auto; # one worker per CPU core worker_rlimit_nofile 1024; # tight file-descriptor cap # Log to stdout/stderr so Docker can collect them error_log /dev/stderr warn; pid /tmp/nginx.pid; # writable by non-root user events { worker_connections 512; use epoll; multi_accept on; } http { # ── Temp paths writable by the nginx user ─────────────────────────────── # /tmp is a 1777 tmpfs — writable by any user, no subdirectory pre-creation needed client_body_temp_path /tmp/client_temp; proxy_temp_path /tmp/proxy_temp; fastcgi_temp_path /tmp/fastcgi_temp; uwsgi_temp_path /tmp/uwsgi_temp; scgi_temp_path /tmp/scgi_temp; access_log /dev/stdout combined; # ── MIME types ────────────────────────────────────────────────────────── include /etc/nginx/mime.types; default_type application/octet-stream; # ── Performance ───────────────────────────────────────────────────────── sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 30; keepalive_requests 100; # ── Compression ───────────────────────────────────────────────────────── gzip on; gzip_static on; gzip_vary on; gzip_min_length 256; gzip_proxied any; gzip_comp_level 5; gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml font/woff font/woff2; # ── Security headers ──────────────────────────────────────────────────── server_tokens off; # hide nginx version # ── Virtual host ──────────────────────────────────────────────────────── server { listen 8080; server_name _; root /usr/share/nginx/html; index index.html; # Security headers sent on every response add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; add_header Content-Security-Policy "default-src 'self'; \ script-src 'self' 'unsafe-inline'; \ style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \ font-src 'self' https://fonts.gstatic.com; \ img-src 'self' data:; \ connect-src 'self'; \ frame-ancestors 'none';" always; # Cache static assets aggressively, HTML never location ~* \.(css|js|woff2?|ico|svg|png|jpg|jpeg|gif|webp)$ { expires 1y; add_header Cache-Control "public, immutable"; } location / { try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache, no-store, must-revalidate"; } # Deny access to hidden files location ~ /\. { deny all; } # Limit accepted HTTP methods if ($request_method !~ ^(GET|HEAD)$) { return 405; } # Cap request body (nothing to POST to a static site) client_max_body_size 1k; # Timeout tuning client_header_timeout 10s; client_body_timeout 10s; send_timeout 10s; } }