KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Serving Static Sites with Nginx

2026/05/11 in Nginx

If your site is built with Hugo, Jekyll, Eleventy, Astro, or just plain HTML and CSS, you don't need PHP, you don't need a database, and you definitely don't need a 300MB WordPress install. Nginx serving static files is what it was built to do — and it does it at a scale that most CMS setups will never touch.

Who this is for

  • Static site generators: Hugo, Jekyll, Eleventy, Astro, Zola, Pelican, Gatsby, Next.js (static export)
  • Frontend framework builds: Bootstrap, Tailwind CSS, Bulma, or any hand-rolled HTML/CSS/JS
  • Use cases: landing pages, documentation, portfolios, personal blogs, brochure sites, "coming soon" pages — anything that doesn't need server-side logic

The appeal

A static site has no PHP-FPM pool to exhaust, no database to overwhelm, no login panels to brute-force. The attack surface is the filesystem and Nginx itself — both of which are battle-tested. Caching is trivially aggressive because there are no logged-in users to accommodate, no sessions to track, and no dynamically-generated pages that might serve stale content. The same server that struggles under a modest WordPress traffic spike can serve a static site to tens of thousands of concurrent visitors without breaking a sweat.


Step 1: The minimal server block

This is the entire configuration for a static site:

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;
    ssl_trusted_certificate /etc/letsencrypt/live/mysite.com/chain.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;

    location / {
        try_files $uri $uri/ =404;
    }
}

That's it. No FastCGI, no proxy_pass, no PHP handler. Point root at your build output directory and Nginx handles the rest.

Compare this to a typical CMS server block — no fastcgi_pass, no limit_conn for PHP, no client_max_body_size for uploads, no database connection overhead per request. Each request is a filesystem lookup and a sendfile() call.


Step 2: Clean URLs — drop the .html extension

Most static site generators produce pages as about.html, contact.html, and so on. You probably want visitors to see /about/ instead of /about.html. Nginx can do this with one line:

location / {
    try_files $uri $uri.html $uri/ =404;
}

Now a request for /about looks for about.html and serves it transparently. The browser never sees the .html extension.

If your generator outputs clean directories instead (/about/index.html), use:

location / {
    try_files $uri $uri/ =404;
}

The trailing $uri/ catches directory-index requests — /about/ looks for /about/index.html automatically.


Step 2.5: Block anything that isn't a GET or HEAD request

A static site has no forms, no API endpoints, no comment submissions — nothing on the server knows what to do with a POST, PUT, PATCH, or DELETE. You can reject those methods at the door rather than letting Nginx silently serve a file in response to a malformed request:

location / {
    if ($request_method !~ ^(GET|HEAD)$) {
        return 405;
    }
    try_files $uri $uri.html $uri/ =404;
}

A 405 Method Not Allowed is the correct HTTP response here — it tells the client "I understood what you asked for, but this server doesn't do that." Compare this to a CMS setup, where POST requests to index.php are expected (login forms, search, comments) and a blanket method restriction would break the site. For a static site, there's nothing to break — and blocking non-read methods reduces the attack surface further.


Step 3: Compress everything ahead of time

Nginx can compress responses on the fly with gzip on, but for a static site you can do better: pre-compress your files during the build step and let Nginx serve the compressed versions directly. No CPU spent per request.

During your build, generate .gz versions of your text-based assets — most static site generators and bundlers can do this automatically. Then enable:

gzip_static on;

When a browser requests style.css and sends Accept-Encoding: gzip, Nginx looks for style.css.gz on disk and serves it directly — matching the original's headers and mimetype.

If you've compiled Nginx with the Brotli module, you can serve pre-compressed .br files the same way:

brotli_static on;

Brotli typically produces 15–20% smaller files than gzip at the same compression level. Modern browsers have supported it since 2017.


Step 4: Aggressive caching

Static sites have a superpower: assets that change get new filenames. Most build tools append a content hash — main.a1b2c3d.js — so when the content changes, the filename changes too. This means you can cache everything indefinitely without worrying about stale content.

# Hashed assets — immutable, cache forever
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 but still 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|html|xml|txt)$ {
    expires 7d;
    add_header Cache-Control "public, must-revalidate";
}

The first block catches hashed filenames — immutable tells the browser "this file will never change, don't even ask." The second and third cover non-hashed assets with shorter durations and revalidation.

If your build pipeline hashes everything (recommended), you can simplify to just the first block plus a shorter fallback for .html files.

  • ← Previous
  • 1
  • 2
  • 3
  • Next →
Tags: nginx, static, hugo, jekyll, eleventy, astro, caching, gzip, brotli
©2026 KBeezie | Disclaimer | Privacy Notice