What I Was Trying to Do
I set up nginx on a VPS to serve a Next.js static export, but every URL returned 404 Not Found. The config was copied straight from the docs, so I had no idea where to start. It took two hours to track down the actual cause.
Environment
- Ubuntu 22.04
- nginx 1.18.0
- Next.js 14.x (static export, output to
out/directory)
Common Causes of nginx 404
Cause 1: The root path doesn’t exist or is empty
server {
listen 80;
server_name example.com;
root /var/www/html/out;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Verify the directory actually exists:
ls -la /var/www/html/out
If it’s missing or empty, nginx has nothing to serve and returns 404. For Next.js, you need to run npm run build with the static export config before copying the files.
Cause 2: try_files ends with =404 in a SPA
# This breaks all routes in a SPA
location / {
try_files $uri $uri/ =404;
}
# Correct for SPA: fall back to index.html
location / {
try_files $uri $uri/ /index.html;
}
Using =404 as the last fallback means any URL without a matching file returns 404 immediately. React, Next.js, and Vue SPAs route client-side, so you need to fall back to index.html.
Cause 3: File permissions are too restrictive
# nginx runs as www-data
ls -la /var/www/html/out/
# Set correct ownership and permissions
chmod -R 755 /var/www/html/out
chown -R www-data:www-data /var/www/html/out
If the owner is still root, nginx can’t read the files and returns 403 or 404.
Cause 4: Confusing root and alias
# root includes the location path in the file lookup
location /static/ {
root /var/www/files;
# /static/foo.js → looks for /var/www/files/static/foo.js
}
# alias strips the location path
location /static/ {
alias /var/www/files/;
# /static/foo.js → looks for /var/www/files/foo.js
}
Using root when you meant alias will cause nginx to look in the wrong place every time.
What I Tried First
I kept running systemctl restart nginx after each config change, but nothing changed. It turned out there was a syntax error in the config, and nginx was running the old version the whole time.
# Always check syntax before reloading
nginx -t
# Reload if syntax is OK
systemctl reload nginx
I also ignored the error log for way too long. Looking at it immediately would have saved most of the debugging time.
tail -f /var/log/nginx/error.log
The log clearly said:
2026/06/07 10:23:14 [error] 1234#1234: *1 "/var/www/html/out/about/index.html" is not found
That’s the whole story right there.
The Fix
Running nginx -t first revealed the syntax error. After fixing the root path and permissions, everything worked.
nginx -t
ls -la /var/www/html/out/index.html
chown -R www-data:www-data /var/www/html/out
chmod -R 755 /var/www/html/out
systemctl reload nginx
If the error log shows is not found, the root path or file is missing. If it shows Permission denied, fix ownership and permissions. This fixed it.
Key Takeaways
- Always run
nginx -tbefore reloading — syntax errors silently keep nginx on the old config - Check the error log first:
/var/log/nginx/error.logusually states exactly what file nginx couldn’t find - SPAs need
try_files $uri $uri/ /index.html, not=404— the latter breaks all non-root routes - Both
chownandchmodmust be correct; fixing only one still blocks access rootandaliasbehave differently:rootappends the location path,aliasstrips it — mixing them up is a common source of silent 404s
Related Articles
- nginx Basic Configuration File Guide
- nginx 502 Bad Gateway: Causes and How to Fix It
- nginx location Directives Explained
- nginx Reverse Proxy Setup
- Linux File Permissions Guide (chmod/chown)
Recommended Cloud Hosting
Looking for reliable cloud infrastructure? Check out these developer-friendly services.
- Cherry Servers - High-performance VPS and dedicated servers
- Cloudways - Managed cloud hosting for developers