The Full Configuration
Here's everything together:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mysite.com;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
root /var/www/mysite.com/public_html;
index index.html;
access_log /var/log/nginx/mysite.access.log;
error_log /var/log/nginx/mysite.error.log;
server_tokens off;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "default-src 'self'; "
"img-src 'self' https://*; "
"style-src 'self' 'unsafe-inline'; "
"script-src 'self'; "
"font-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self';" always;
# Pre-compressed assets
gzip_static on;
# Let's Encrypt
location ~ /.well-known/acme-challenge {
root /usr/share/nginx/html;
allow all;
}
# Automated probes
location = /.well-known/traffic-advice { return 204; }
location = /.well-known/tdmrep.json { return 204; }
# Clean URLs — drop .html extension
location / {
if ($request_method !~ ^(GET|HEAD)$) {
return 405;
}
try_files $uri $uri.html $uri/ =404;
}
# Hashed assets — immutable
location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|ico|webp|svg|woff2|woff)$ {
expires max;
add_header Cache-Control "public, immutable";
}
# Non-hashed static — cache with revalidation
location ~* \.(png|jpg|jpeg|gif|ico|webp|woff2|woff|ttf|svg)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
location ~* \.(css|js|xml|txt)$ {
expires 7d;
add_header Cache-Control "public, must-revalidate";
}
# Housekeeping
location = /favicon.ico {
access_log off;
log_not_found off;
try_files $uri =204;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ ~$ {
deny all;
access_log off;
log_not_found off;
}
location ~* \.(yml|yaml|env|bak|swp|dist|config|tfstate|tfvars)$ {
deny all;
access_log off;
log_not_found off;
}
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
}
No PHP. No database. No CMS updates. Just Nginx doing what it's always been best at — serving files fast, securely, and without drama.
This guide is maintained as part of a modular, SSL-first framework. Each configuration is audited for production stability and modern security standards.
Compatibility: Tested against current stable releases. While optimized for the stack above, core logic remains relevant for Nginx 1.26+.
2026-05-21: Production audit — bumped Nginx to 1.30.1. Added .well-known/traffic-advice and tdmrep.json exceptions to the housekeeping block to prevent Chrome and AI crawler probes from generating 404 noise. Added tfstate and tfvars to the sensitive extension deny list. Added cross-link to the Securing Nginx and PHP guide for the full drop.conf with rate-limited deny blocks.