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
# 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
- Identify the exact failing URL
Test the same URL the browser is requesting:bashcurl -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. - 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. - Check your Flask path settings
Confirm your application generates URLs that match what Nginx serves.
Example Flask app setup:pythonfrom 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. - Verify the file exists on disk
Check the real file path, not just the directory:bashls -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 - Review the active Nginx server block
Dump the loaded config instead of trusting one file:bashsudo nginx -T
Look for the correctserver_name,location /static/, andlocation /media/blocks. - Use
aliascorrectly for prefixed URLs
For/static/...and/media/...,aliasis usually the safest option:nginxserver { 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/
- Avoid incorrect
rootusagerootandaliasdo not resolve paths the same way.
Example:nginxlocation /static/ { root /var/www/yourapp; }
A request for/static/app.cssresolves to:text/var/www/yourapp/static/app.css
But:nginxlocation /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. - Check directory and file permissions
Nginx must be able to traverse parent directories and read the target file:bashnamei -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 benginxinstead ofwww-data. - 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:bashnpm run build cp -r dist/* /var/www/yourapp/static/
If the files were never copied, URLs may be correct but still return 404. - 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:pythonapp.config["UPLOAD_FOLDER"] = "/var/www/yourapp/media/uploads"
Matching Nginx:nginxlocation /media/ { alias /var/www/yourapp/media/; } - 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:bashgrep -R "location /static\|location /media\|server_name" /etc/nginx/sites-enabled /etc/nginx/conf.d sudo nginx -T - Validate and reload Nginxbash
sudo nginx -t sudo systemctl reload nginx
Then retest the exact URLs:bashcurl -I https://your-domain.com/static/app.css curl -I https://your-domain.com/media/uploads/example.jpg - Check container mounts or symlinks if applicable
If using Docker or symlinked directories, confirm the files exist where Nginx sees them:bashls -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 thealiasto the real absolute path. - Using
rootinstead ofaliasincorrectly → Nginx builds an unexpected final path → switch toaliasor recalculate the resolved path. - Missing trailing slash in
aliasorlocation→ path resolution breaks subtly → keeplocation /static/withalias /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 -Tand 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:
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:
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/...returning404in Nginx access logs - Error log messages showing missing file paths
- A different
server_nameblock 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 -tpasses without errors - Nginx has been reloaded after config changes
- The browser now returns
200or304for the asset URL
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Static Files Not Loading in Production
- Flask Static and Media Files Production Setup
- Flask Production Checklist (Everything You Must Do)
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.