Serving Static Files

Introduction

Nginx excels at delivering static files—HTML, CSS, JavaScript, images, fonts, and PDFs—directly from disk with low overhead. This chapter covers root, try_files, deploying frontend build output, SPA client-side routing, and custom 404 pages—the foundation for every static or frontend deployment in this track.

Prerequisites

root vs alias (Quick Comparison)

Both map URLs to filesystem paths:

nginx
location / {
    root /var/www/myapp;
}

Request /css/app.css → file /var/www/myapp/css/app.css.

nginx
location /assets/ {
    alias /var/www/myapp/build/assets/;
}

Request /assets/logo.png → file /var/www/myapp/build/assets/logo.png (no extra assets segment in the path).

Code explanation:

  • root appends the full URI to the path
  • alias replaces the matched location prefix—useful when URL path ≠ folder name

For whole-site static hosting, root is the default choice.

Basic Static Site with try_files

nginx
server {
    listen 80;
    server_name static.example.com;
 
    root /var/www/static;
    index index.html;
 
    location / {
        try_files $uri $uri/ =404;
    }
}

Code explanation:

  • $uri — try exact file (/about.html)
  • $uri/ — try directory with index (/docs//docs/index.html)
  • =404 — return 404 if neither exists

Deploy a Frontend Build (dist/)

After npm run build (Vite, React, Vue, etc.):

bash
sudo mkdir -p /var/www/myapp
sudo rsync -av --delete ./dist/ /var/www/myapp/
sudo chown -R www-data:www-data /var/www/myapp

Nginx config:

nginx
server {
    listen 80;
    server_name app.example.com;
 
    root /var/www/myapp;
    index index.html;
 
    location / {
        try_files $uri $uri/ =404;
    }
}

Code explanation:

  • rsync --delete removes old hashed assets from previous builds
  • Single-page apps need a different try_files—see below

See also HTML static site deployment for hosting options beyond a VPS.

Single-Page Application (SPA) Routing

React Router, Vue Router, and similar clients handle routes in the browser. Direct refresh on /dashboard must still return index.html:

nginx
location / {
    try_files $uri $uri/ /index.html;
}

Code explanation:

  • Unknown paths fall back to /index.html so the JS router can run
  • Static assets with real filenames (/assets/index-abc123.js) still match $uri first

Warning

API Paths on Same Host

If /api/ goes to a backend, use a separate location /api/ with proxy_pass before or with higher priority than /. Covered in reverse proxy chapters.

Serving a Subpath Only

Site lives at https://example.com/docs/:

nginx
location /docs/ {
    alias /var/www/handbook/;
    try_files $uri $uri/ /docs/index.html;
}

Trailing slashes on location and alias must align—misconfiguration causes 404 loops or wrong paths.

Custom 404 Page

nginx
error_page 404 /404.html;
 
location = /404.html {
    root /var/www/myapp;
    internal;
}

Create /var/www/myapp/404.html with a friendly message and link home.

Code explanation:

  • error_page 404 intercepts not-found responses
  • internal (optional) restricts direct access patterns; simple static 404 pages often omit it

Directory Listing (Usually Off)

nginx
location / {
    autoindex off;
}

autoindex on shows file lists—convenient for temp shares, risky for production. Keep it off unless intentional.

Security: Block Sensitive Paths

nginx
location ~ /\. {
    deny all;
}
 
location ~* \.(env|git|sql)$ {
    deny all;
}

Code explanation:

  • Regex location ~ denies hidden files (.git, .env)
  • Prevents accidental deployment of secrets or VCS metadata

Full Example: Static Site server Block

Deploy Checklist

  • Build output copied to server (rsync or CI)
  • root points at the folder containing index.html
  • Permissions: directories 755, files 644, owner readable by www-data
  • SPA uses try_files ... /index.html if client routing exists
  • sudo nginx -t && sudo systemctl reload nginx
  • curl -I http://your-domain/ returns 200

FAQ

Why 403 instead of 404?

Often permissions—Nginx user cannot read directory or file. Check namei -l /var/www/... or ls -la.

Should I gzip here?

Enable gzip in a dedicated chapter—usually in the http or server block for text assets.

Can Nginx serve from my home directory?

Possible but not recommended for production. Use /var/www/ or /srv/ with correct ownership.

How do I cache bust hashed filenames?

Vite/Webpack emit app.a1b2c3.js—safe for long cache headers on /assets/ while keeping index.html short-lived (next chapter).

root inside location vs server?

server-level root inherits into locations unless overridden.

Same server for HTML and API?

Yes—location /api/ { proxy_pass ... } plus location / { try_files ... } on one server_name.