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
- Your First Virtual Host
- Nginx Configuration Syntax
- A built static site folder (for example
dist/from Vite) or sample HTML files
root vs alias (Quick Comparison)
Both map URLs to filesystem paths:
location / {
root /var/www/myapp;
}Request /css/app.css → file /var/www/myapp/css/app.css.
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:
rootappends the full URI to the pathaliasreplaces 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
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 withindex(/docs/→/docs/index.html)=404— return 404 if neither exists
Deploy a Frontend Build (dist/)
After npm run build (Vite, React, Vue, etc.):
sudo mkdir -p /var/www/myapp
sudo rsync -av --delete ./dist/ /var/www/myapp/
sudo chown -R www-data:www-data /var/www/myappNginx config:
server {
listen 80;
server_name app.example.com;
root /var/www/myapp;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Code explanation:
rsync --deleteremoves 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:
location / {
try_files $uri $uri/ /index.html;
}Code explanation:
- Unknown paths fall back to
/index.htmlso the JS router can run - Static assets with real filenames (
/assets/index-abc123.js) still match$urifirst
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/:
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
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 404intercepts not-found responsesinternal(optional) restricts direct access patterns; simple static 404 pages often omit it
Directory Listing (Usually Off)
location / {
autoindex off;
}autoindex on shows file lists—convenient for temp shares, risky for production. Keep it off unless intentional.
Security: Block Sensitive Paths
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 (
rsyncor CI) -
rootpoints at the folder containingindex.html - Permissions: directories
755, files644, owner readable bywww-data - SPA uses
try_files ... /index.htmlif 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.