Nginx Server Configuration for Static Sites
I use nginx as my webserver. It’s fast and has a tone of features. However, for me it’s been a process to learn how to set up properly redirecting, caching, secure, and compressed static webpages with it. I’m not web developer. It’s been slow for me to discover all the useful things it can do for me.
Anyhow, over the past few years I’ve started to get a grasp on what features are useful to implement for the server blocks in nginx. I thought I would share them on my newsletter for anyone else looking on what features to include for hosting a static site. This newsletter article is also for myself for future reference.
HTTP2:
listen 443 ssl http2;
Adding http2
after the listen 443 ssl
in the https code block will enable http2 for your site. This can make your site load a lost faster.
Security Headers:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; img-src *; media-src *; https://example.com/css/ "
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
server_tokens off;
Strict-Transport-Security (HSTS):
- Forces all connections to use HTTPS after the first secure visit that happened within a year ago.
- Includes subdomains and preloads, ensuring immediate enforcement of HTTPS.
Content-Security-Policy (CSP):
- Restricts resource sources.
- Default-src ‘self’ allows only same-origin content by default. This means that by default, external content (e.g. javascript, fonts, stylesheets) that can be linked on the webpages hosted have to be sources that have the same scheme, same host, and same port as the file the content policy is defined in.
- img-src and media-src allow any sources, which is potentially risky but allows hotlinking images and other media from external sites. CSS is allowed to load from a CSS folder.
X-Frame-Options:
- Prevents clickjacking by restricting framing of the page to same-origin sites only.
X-Content-Type-Options:
- Stops MIME type guessing, reducing potential vulnerabilities like XSS.
Referrer-Policy:
- Protects against information leakage by not sending referrer data during HTTP downgrade.
server_tokens off:
- Hides server details to reduce exposure of software versions and potential attack vectors.
GZIP:
gzip on;
gzip_min_length 1100;
gzip_buffers 4 32k;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
- Gzip compresses the text on webpages so that it takes less bandwidth to serve webpages.
URL Redirection Logic:
location / {
try_files $uri $uri.html $uri/ =404;
error_page 404 = /error404;
if ($request_uri ~ ^/(.*)\.html(\?|$)) {
return 302 /$1;
}
}
if ($request_uri ~ ^/(.*)\.html(\?|$)) {
return 302 /$1;
}
- This directive attempts to serve the requested URI, then appends
.html
if the file isn’t found, then checks if a directory exists, and finally returns a 404 error if none are available. The configuration includes an if block that checks if the requested URL ends with .html and, if so, issues a 302 redirect to the URL without the .html extension.
HTTP traffic Redirect to HTTPS:
if ($host = example.com) {
return 301 https://$host$request_uri;
}
- This is placed into the server block that listens to http requests (listen 80) and redirects the traffic to the https server block (listen 443).
More Behavior For Linked Content:
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|webp|otf)$ {
expires 6M;
access_log off;
add_header Cache-Control "public, immutable";
try_files $uri $uri/ =404;
# Persistent connections
keepalive_timeout 65;
keepalive_requests 100;
}
Caching Behavior
expires 6M;
: Sets the default expiration time for these files to 6 months (6M
). This tells browsers and caches to store these static assets locally for up to 6 months before requesting a fresh copy.access_log off;
: Disables access logging for requests to these static assets, reducing server load and log file size.add_header Cache-Control "public, immutable";
:public
: Allows public caches (e.g., CDN caches) to cache the resource.immutable
: Informs the browser that this resource does not change and can be cached indefinitely. This is ideal for static assets like images and fonts.
Static File Handling
-
try_files $uri $uri/ =404;
:- First, it attempts to find the file at
$uri
(the requested path). - If not found, it tries
$uri/
(to handle cases where a trailing slash is missing). - If neither exists, it returns a
404 Not Found
error.
- First, it attempts to find the file at
-
This ensures that requests for non-existent files or paths are properly handled.
Persistent Connections
-
keepalive_timeout 65;
: Configures the server to maintain a keep-alive connection with clients for up to 65 seconds. -
keepalive_requests 100;
: Limits the number of simultaneous requests on a single connection to 100. -
These directives optimize performance by allowing multiple requests to be handled over a single TCP connection, reducing overhead.
Disable nginx_status
location = /nginx_status {
if ($http_user_agent ~* "Netdata") {
return 403;
}
}
- nginx_status gets spammed by bots often, so it’s best to just disable the page.
Complete Server Block(s) Example:
server {
server_name example.com;
root /var/website;
index index.html index.htm index.nginx-debian.html;
location / {
try_files $uri $uri.html $uri/ =404;
error_page 404 = /error404;
if ($request_uri ~ ^/(.*)\.html(\?|$)) {
return 302 /$1;
}
}
location = /nginx_status {
if ($http_user_agent ~* "Netdata") {
return 403;
}
}
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|webp|otf)$ {
expires 6M;
access_log off;
add_header Cache-Control "public, immutable";
try_files $uri $uri/ =404;
# Persistent connections
keepalive_timeout 65;
keepalive_requests 100;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; img-src *; media-src *; https://example.com/css/"
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
server_tokens off;
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl http2; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
gzip on;
gzip_min_length 1100;
gzip_buffers 4 32k;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
if ($host = example.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name example.com;
return 404; # managed by Certbot
}
Securty headers and gzip should be added into a separate http server block in the nginx configuration if nginx is managing multiple sites on the same VPS. Certbot is being used to implement https for the website in this example.