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— the URL path is appended to it: a request for/topics/git/index.htmlserves the file/var/www/mysite/topics/git/index.html.try_files $uri $uri/ =404;— try the exact file, then a directory of that name (serving itsindex), else return 404. For single-page apps, the varianttry_files $uri /index.html;routes unknown paths to the app shell.- The regex location (
~*= case-insensitive) — static assets get long-lived cache headers so browsers stop re-downloading them. Safe when filenames are versioned (style.css?v=123or hashed names).
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.