bchanot-cv/nginx.conf
Bastien Chanot f1e4392c65 fix(docker): COPY favicon assets into image + cache header
Dockerfile selectively COPYs files into /usr/share/nginx/html. Favicon
assets (favicon.svg, favicon-32.png, favicon.ico, apple-touch-icon.png)
were added to the repo in ef31fb3 but never wired into the Dockerfile,
so a rebuilt container served 404 for /favicon.svg and friends — broken
favicon in prod even after `docker compose up -d --build`.

nginx.conf gets a matching long-cache rule for icon/image assets
(30 days, immutable, access_log off) — they rarely change and the file
name is the cache key anyway.

Deploy: on the VPS, `docker compose up -d --build`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 04:07:32 +02:00

79 lines
2.6 KiB
Nginx Configuration File

# nginx server block for bchanot.fr static site.
# Container listens on port 80; host port is configured via docker-compose
# (PORT env var). A host-level reverse proxy (nginx, Traefik, Caddy) should
# terminate TLS and proxy_pass to http://127.0.0.1:${PORT}.
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers. HSTS is intentionally NOT set here — leave it to the
# outer reverse proxy that terminates TLS, otherwise it may be sent over
# plain HTTP between proxy and container.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
# CSP: inline CSS + JS are allowed (project convention), fonts from Google.
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
# Forwarded headers — trust the upstream reverse proxy.
real_ip_header X-Forwarded-For;
set_real_ip_from 0.0.0.0/0;
# Compression.
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/html
text/javascript
application/javascript
application/json
application/xml
application/pdf
image/svg+xml;
# Long cache for the PDF (regenerated rarely, content-hash not used).
location ~* \.pdf$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
# Short cache for HTML so content updates land fast.
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public, max-age=3600, must-revalidate";
}
# Long cache for favicon + image assets (rarely change).
location ~* \.(?:ico|svg|png|jpg|jpeg|gif|webp)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
access_log off;
}
# Logs to stdout/stderr (default in nginx:alpine).
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
location ~ /\. {
deny all;
return 404;
}
# Default: serve files, fall back to 404.
location / {
try_files $uri $uri/ =404;
}
}