My Server Journal

A minimal nginx config for a static site

Every nginx tutorial I found either stops at "it works" or adds fifty lines of cargo cult. This is what a static site on one domain actually needs: an HTTP block that redirects, an HTTPS block that serves files, a handful of headers, and nothing else.

Layout

/var/www/example.com/     # the site, owned by root, world-readable
/etc/nginx/sites-available/example.com
/etc/nginx/sites-enabled/example.com -> ../sites-available/example.com

Delete sites-enabled/default; it answers on port 80 for any name and will confuse you later.

The config

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # ACME challenges are served here so renewals keep working
    location /.well-known/acme-challenge/ {
        root /var/www/acme;
    }
    location / {
        return 301 https://example.com$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate     /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;

    root /var/www/example.com;
    index index.html;

    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
    add_header Strict-Transport-Security "max-age=31536000" always;

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

Two notes on that. http2 on; is the modern form (nginx 1.25+); older versions want listen 443 ssl http2;. And the www name only appears in the port-80 block on purpose: it redirects to the bare domain, so there is exactly one canonical URL for every page.

Headers I did not add

A Content-Security-Policy is worth it once there is JavaScript to constrain. For plain HTML and one stylesheet it adds nothing but a header to get wrong. Same for X-Frame-Options: nobody is framing a notes site, and if they do, fine.

Checking it

sudo nginx -t && sudo systemctl reload nginx
curl -sI http://example.com/ | head -1        # HTTP/1.1 301 Moved Permanently
curl -sI https://example.com/ | head -1       # HTTP/2 200
curl -sI https://example.com/missing | head -1 # HTTP/2 404

Also server_tokens off; in nginx.conf, purely so the version number is not advertised in every response. It is not security; it is just tidy.