Reference Guide Detailed deployment notes with production context and concrete examples.

Flask 404 on Static or Media Files

If you're getting 404 errors for CSS, JavaScript, images, or uploaded files in production, this guide shows you how to trace the request path and fix the serving configuration step-by-step. The goal is to make static and media URLs resolve correctly through Nginx and Flask.

Quick Fix / Quick Setup

bash
# 1) Confirm the requested URL path matches your Nginx config
curl -I https://your-domain.com/static/app.css
curl -I https://your-domain.com/media/uploads/example.jpg

# 2) Check Nginx location blocks
sudo nginx -T | sed -n '/server_name your-domain.com/,/}/p'

# Example working config
server {
    server_name your-domain.com;

    location /static/ {
        alias /var/www/yourapp/static/;
        expires 30d;
        access_log off;
    }

    location /media/ {
        alias /var/www/yourapp/media/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

# 3) Verify files actually exist on disk
ls -lah /var/www/yourapp/static/
ls -lah /var/www/yourapp/media/

# 4) Test and reload Nginx
sudo nginx -t && sudo systemctl reload nginx

Most 404 cases are caused by a mismatch between the URL path, the Nginx location block, and the real filesystem path. Fix those three first.

What’s Happening

A 404 on static or media files means the request reaches the server, but the server cannot map that URL to an existing file or route. In production, Nginx usually serves static assets and uploaded media directly, while Gunicorn only handles Flask application requests. If the URL prefix, Nginx alias/root, filesystem path, or Flask upload/static settings do not match, the request returns 404.

Step-by-Step Guide

  1. Identify the exact failing URL
    Test the same URL the browser is requesting:
    bash
    curl -I https://your-domain.com/static/app.css
    curl -I https://your-domain.com/media/uploads/example.jpg
    

    Use browser dev tools if needed to confirm the exact request path.
  2. Confirm whether the file is static or media
    • Static: CSS, JS, built frontend assets, app images
    • Media: user uploads, generated files

    These should usually be served from separate directories.
  3. Check your Flask path settings
    Confirm your application generates URLs that match what Nginx serves.
    Example Flask app setup:
    python
    from flask import Flask
    
    app = Flask(__name__, static_url_path="/static", static_folder="static")
    app.config["UPLOAD_FOLDER"] = "/var/www/yourapp/media"
    

    If templates generate /static/ but Nginx serves /assets/, requests will 404.
  4. Verify the file exists on disk
    Check the real file path, not just the directory:
    bash
    ls -lah /var/www/yourapp/static/
    ls -lah /var/www/yourapp/media/
    stat /var/www/yourapp/static/app.css
    stat /var/www/yourapp/media/uploads/example.jpg
    
  5. Review the active Nginx server block
    Dump the loaded config instead of trusting one file:
    bash
    sudo nginx -T
    

    Look for the correct server_name, location /static/, and location /media/ blocks.
  6. Use alias correctly for prefixed URLs
    For /static/... and /media/..., alias is usually the safest option:
    nginx
    server {
        server_name your-domain.com;
    
        location /static/ {
            alias /var/www/yourapp/static/;
            expires 30d;
            access_log off;
        }
    
        location /media/ {
            alias /var/www/yourapp/media/;
        }
    
        location / {
            include proxy_params;
            proxy_pass http://unix:/run/gunicorn.sock;
        }
    }
    

    Keep trailing slashes consistent:
    • location /static/
    • alias /var/www/yourapp/static/
  7. Avoid incorrect root usage
    root and alias do not resolve paths the same way.
    Example:
    nginx
    location /static/ {
        root /var/www/yourapp;
    }
    

    A request for /static/app.css resolves to:
    text
    /var/www/yourapp/static/app.css
    

    But:
    nginx
    location /static/ {
        alias /var/www/yourapp/static/;
    }
    

    The same request resolves to:
    text
    /var/www/yourapp/static/app.css
    

    If you mix these patterns incorrectly, the final filesystem path becomes wrong.
  8. Check directory and file permissions
    Nginx must be able to traverse parent directories and read the target file:
    bash
    namei -l /var/www/yourapp/static/app.css
    sudo -u www-data test -r /var/www/yourapp/static/app.css && echo readable
    sudo -u www-data test -r /var/www/yourapp/media/uploads/example.jpg && echo readable
    

    On some systems, the Nginx user may be nginx instead of www-data.
  9. Confirm built or collected assets were deployed
    If your assets are generated during build, make sure the output files exist in the directory referenced by Nginx.
    Example:
    bash
    npm run build
    cp -r dist/* /var/www/yourapp/static/
    

    If the files were never copied, URLs may be correct but still return 404.
  10. Confirm uploads are written to the same directory Nginx serves
    If Flask saves uploads to one directory and Nginx serves another, uploaded files will appear missing.
    Example Flask upload path:
    python
    app.config["UPLOAD_FOLDER"] = "/var/www/yourapp/media/uploads"
    

    Matching Nginx:
    nginx
    location /media/ {
        alias /var/www/yourapp/media/;
    }
    
  11. Check if requests are bypassing the static/media blocks
    If /static/ or /media/ requests are going to Gunicorn, your Nginx location blocks may be missing or in the wrong server block.
    Check access patterns and loaded config:
    bash
    grep -R "location /static\|location /media\|server_name" /etc/nginx/sites-enabled /etc/nginx/conf.d
    sudo nginx -T
    
  12. Validate and reload Nginx
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    

    Then retest the exact URLs:
    bash
    curl -I https://your-domain.com/static/app.css
    curl -I https://your-domain.com/media/uploads/example.jpg
    
  13. Check container mounts or symlinks if applicable
    If using Docker or symlinked directories, confirm the files exist where Nginx sees them:
    bash
    ls -lah /var/www/yourapp/static/
    ls -lah /var/www/yourapp/media/
    

    On containerized setups, verify bind mounts inside the running container, not only on the host.

Common Causes

  • Wrong Nginx alias path → Nginx maps /static/ or /media/ to the wrong directory → correct the alias to the real absolute path.
  • Using root instead of alias incorrectly → Nginx builds an unexpected final path → switch to alias or recalculate the resolved path.
  • Missing trailing slash in alias or location → path resolution breaks subtly → keep location /static/ with alias /path/to/static/.
  • Files were never deployed or collected → URLs are correct but files do not exist → rebuild or copy assets to the production directory.
  • Media uploads saved to a different folder than Nginx serves → uploaded files exist elsewhere → align application upload path with Nginx media path.
  • Wrong server block is active → edits were made in one file but another vhost handles the domain → inspect nginx -T and remove conflicting configs.
  • Permissions on directories or files are too restrictive → Nginx cannot traverse or read files and may surface 404 behavior → fix ownership and mode bits.
  • Template-generated URLs are wrong → browser requests /staticfiles/ while Nginx serves /static/ → correct URL generation in the app or templates.
  • Docker volume or bind mount missing → files exist on host but not in the container seen by Nginx → fix the mount path.
  • Symlink target missing or unreadable → Nginx follows a broken path → repair the symlink or serve the real directory directly.

Debugging Section

Check the active configuration and logs first:

bash
sudo nginx -T
sudo nginx -t
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

Useful path and permission checks:

bash
ls -lah /var/www/yourapp/static/
ls -lah /var/www/yourapp/media/
stat /var/www/yourapp/static/app.css
namei -l /var/www/yourapp/static/app.css
sudo -u www-data test -r /var/www/yourapp/static/app.css && echo readable

What to look for:

  • Requests for /static/... or /media/... returning 404 in Nginx access logs
  • Error log messages showing missing file paths
  • A different server_name block handling the request
  • Gunicorn receiving static/media requests that Nginx should serve
  • Rendered HTML pointing to the wrong asset prefix

Checklist

  • The failing URL path is identified exactly from browser dev tools or curl
  • Static files and media files are mapped separately if both are used
  • Nginx location /static/ points to the real static directory
  • Nginx location /media/ points to the real upload directory
  • The exact missing file exists on disk at the resolved path
  • The Nginx user can read the file and traverse parent directories
  • sudo nginx -t passes without errors
  • Nginx has been reloaded after config changes
  • The browser now returns 200 or 304 for the asset URL

FAQ

Q: Should static and media use the same directory?
A: No. Static assets and user uploads should usually be stored and served from separate directories.

Q: Why does the browser show 404 but Flask logs show nothing?
A: Nginx is likely handling the request directly and returning 404 before it reaches Flask or Gunicorn.

Q: Can Gunicorn serve static files?
A: It can in limited cases, but production setups should normally use Nginx for static and media delivery.

Q: What path should I test first?
A: Test the exact URL the browser requests, then map it directly to the expected filesystem path from the Nginx config.

Final Takeaway

A 404 on static or media files is usually a path-mapping problem, not an application logic problem. Fix the issue by aligning the request URL, the Nginx location block, and the real filesystem path, then validate with curl, nginx -T, and the Nginx logs.