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 -t before reloading — syntax errors silently keep nginx on the old config
  • Check the error log first: /var/log/nginx/error.log usually 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 chown and chmod must be correct; fixing only one still blocks access
  • root and alias behave differently: root appends the location path, alias strips it — mixing them up is a common source of silent 404s

Looking for reliable cloud infrastructure? Check out these developer-friendly services.