Lesson 3 of 6

Serving Static Sites

Serving files is Nginx's home turf — it's what the site you're reading right now does. A complete static-site config is a dozen lines, and every line teaches a directive you'll reuse everywhere else.

💡 The Bottom Line

root maps URLs onto a folder, index names the default file, try_files decides what happens when the file doesn't exist, and a location with expires adds cache headers to assets. That's a production static site.

A complete static-site server block

server {
    listen 80;
    server_name mysite.com;
    root /var/www/mysite;
    index index.html;

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

    location ~* \.(css|js|png|jpg|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    error_page 404 /404.html;
}

root vs alias — the classic confusion

location /docs/ {
    root /var/www/files;    # /docs/a.pdf → /var/www/files/docs/a.pdf
}
location /docs/ {
    alias /var/www/files/;  # /docs/a.pdf → /var/www/files/a.pdf
}

root appends the full URL path; alias replaces the location prefix. When files mysteriously 404 under a prefixed location, this is nearly always the cause. Default to root; reach for alias only when the URL prefix and the folder name genuinely differ.

🙋 There Are No Dumb Questions

Q: Do I need gzip, or is that premature optimization?
A: You want it — text assets (HTML, CSS, JS, JSON) typically shrink 70–80%, and it's four lines in the http block: gzip on; gzip_types text/css application/javascript application/json image/svg+xml;. HTML is compressed by default once gzip on is set. It's one of the highest ratio wins in web performance: four lines, most of your bandwidth.

⚠️ Watch Out

A 403 Forbidden on files you can see on disk is almost always Unix permissions: the Nginx worker runs as user www-data (or nginx), which needs read permission on the files and execute permission on every directory in the path. A home directory with chmod 700 anywhere in the chain blocks everything under it. This is exactly why deploy scripts end with a chown -R www-data + chmod step.

🔧 Try It Yourself

Serve your own folder with the Docker container — mount it over the default root:

mkdir site && echo "<h1>Served by me</h1>" > site/index.html
docker run -d --name mysite -p 8081:80 \
  -v "$(pwd)/site":/usr/share/nginx/html:ro \
  nginx:1.27

Check http://localhost:8081, then request a missing path and watch the 404. Then check response headers with curl -I http://localhost:8081/ — you'll see exactly what Nginx tells browsers about your page.

✅ Quick Check

With location /docs/ { alias /var/www/files/; }, which file does /docs/manual.pdf serve?


Next up: the second hat — reverse proxying requests to a backend app, and load balancing when one backend isn't enough.

← Back to
Next →