Serving Static Files with Nginx: Optimization Tips
Optimize Nginx static file delivery with proper cache headers, Gzip compression, safe defaults, and tips on protecting hidden files — for faster sites with fewer stale-asset headaches.
Serving Static Files with Nginx: Optimization Tips
Serving static files with Nginx is one of the most common and efficient ways to deliver images, CSS, JavaScript, downloads, and built frontend assets. Nginx is very good at this job, but a few configuration choices can make the difference between a fast, predictable site and one that wastes bandwidth or serves stale content.
Static file optimization is mostly about clear paths, correct caching headers, compression, and safe defaults. You do not need a complicated setup to get strong results, but you do need the cache rules to match how your files are named and deployed.
Start With a Clear Static File Location
The simplest static file setup uses root and try_files:
server {
listen 80;
server_name example.com;
root /var/www/example.com/public;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
With this configuration, a request for /css/app.css maps to /var/www/example.com/public/css/app.css. If the file does not exist, Nginx returns 404.
That direct mapping is useful when you are debugging. You can take a URL from the browser, turn it into a filesystem path, and check whether the file exists:
ls -l /var/www/example.com/public/css/app.css
If the file exists but Nginx returns 404, look for a different root, a more specific location block, or an include file that overrides the path.
For a single page app, you may want unknown routes to fall back to index.html:
location / {
try_files $uri $uri/ /index.html;
}
That is useful for frontend routers, but do not use it blindly for every site. If missing assets also return index.html, debugging broken JavaScript or image paths can become confusing. Many teams use a separate location for assets so missing files still return a real 404.
You can also use alias when a URL path should map to a different filesystem path:
location /assets/ {
alias /srv/shared-assets/;
}
Be careful with trailing slashes. With alias, the location path and filesystem path should usually both end with /. A mismatch can produce unexpected file paths.
One safe pattern is:
location /downloads/ {
alias /srv/downloads/;
try_files $uri =404;
}
Here /downloads/manual.pdf maps to /srv/downloads/manual.pdf. Without the trailing slash discipline, it is easy to accidentally build paths that do not exist or expose a directory you did not mean to publish.
For a deeper look at matching behavior, see Nginx location blocks.
Add Browser Caching Headers
Static files are excellent candidates for browser caching. If a user downloads app.css once, the browser should not fetch it again on every page view unless it changed.
For versioned assets, use long cache lifetimes:
location /assets/ {
root /var/www/example.com/public;
expires 1y;
add_header Cache-Control "public, immutable";
}
This works best when filenames change during deployment, such as app.8f3a91.css or bundle.20260523.js. If the filename changes when the content changes, browsers can safely cache the old file for a long time.
For files that keep the same name, use shorter caching:
location = /index.html {
root /var/www/example.com/public;
expires -1;
add_header Cache-Control "no-cache";
}
This pattern is common for frontend apps. The HTML file stays fresh, while hashed CSS and JavaScript files are cached aggressively.
A practical example: your React or Vue build outputs hashed assets in /assets/ and a plain index.html. Cache /assets/ for a year, but make index.html revalidate. Users get fast repeat visits, and new deployments still load the newest asset references.
After changing cache rules, test the headers instead of guessing:
curl -I https://example.com/assets/app.8f3a91.css
curl -I https://example.com/
You want the hashed asset to show a long-lived Cache-Control value. You usually want the HTML page to revalidate or use a short lifetime. If both are cached for a year, a deployment can leave users stuck on an old HTML file that points to old JavaScript.
Use Compression for Text Assets
Text assets such as CSS, JavaScript, SVG, and JSON compress well. You can enable Gzip in the http block:
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_types
text/css
application/javascript
application/json
image/svg+xml;
Do not expect Gzip to help much with JPEG, PNG, WebP, MP4, or zip files. Those formats are already compressed. Trying to compress them again usually wastes CPU.
For high-traffic static sites, consider precompressed files. Your build process can create .gz versions of large CSS and JavaScript files, and Nginx can serve them when the browser supports Gzip:
gzip_static on;
This reduces runtime compression work because Nginx reads the precompressed file from disk. It is most useful when assets are built ahead of time and do not change between requests.
Compression is only one part of asset delivery. File size still matters. Remove unused JavaScript, optimize images during your build process, and avoid shipping large files that users do not need.
When you test compression, include an Accept-Encoding header:
curl -I -H 'Accept-Encoding: gzip' https://example.com/assets/app.js
Look for Content-Encoding: gzip and Vary: Accept-Encoding. The Vary header matters when a CDN or shared cache sits in front of Nginx, because compressed and uncompressed responses must not be mixed up.
Improve File Delivery and Safety
Nginx can serve static files efficiently with default settings, but a few details help in production.
First, disable directory listings unless you explicitly need them:
autoindex off;
Directory listings can reveal filenames and structure that you did not intend to publish.
Second, block access to hidden files such as .env, .git, and other dotfiles:
location ~ /\.(?!well-known) {
deny all;
}
The exception for .well-known is common because certificate validation and standards-based files may use that directory.
Third, make sure your static file permissions allow Nginx to read files but do not give the web server unnecessary write access. A typical setup lets deployment tools write files and lets the Nginx worker user read them.
Fourth, check MIME types. Nginx usually includes a mime.types file, but stripped-down containers or custom builds may miss it. If CSS is served as text/plain, browsers may reject it or behave differently.
Use:
include /etc/nginx/mime.types;
default_type application/octet-stream;
Finally, watch logs for repeated 404 responses on assets. That often means a deploy referenced files that do not exist, a cache still points to an old filename, or an alias path is wrong.
If static delivery feels slow, do not start by copying every tuning directive you can find. First check whether the problem is actually Nginx. Large images, unoptimized frontend bundles, remote storage mounts, and CDN cache misses are more common causes than a missing micro-optimization in the server block.
For a quick local check:
curl -o /dev/null -s -w 'status=%{http_code} size=%{size_download} time=%{time_total}\n' https://example.com/assets/app.js
Then compare that with CDN logs, browser devtools, or a request from the same region as your users. A fast response from Nginx and a slow response in the browser usually points somewhere else in the delivery path.
When to Get Help
Bring in a DevOps engineer if your static files are served from shared storage, mounted volumes, object storage gateways, or a CDN in front of Nginx. The best caching strategy depends on the whole delivery path, not only the Nginx server block.
You should also get help if users report stale JavaScript after deployments. That usually means the cache rules and filename versioning strategy do not match.
Serving static files with Nginx works best when paths are predictable, cache lifetimes match filename strategy, and text assets are compressed. Keep missing files visible, protect hidden files, and test headers after each config change. A clean static setup makes your site faster without adding moving parts.