Flask Media Files Not Serving (Uploads Broken)
If uploaded files work in development but fail in production, this guide shows you how to restore media file delivery step-by-step. It covers the standard Flask + Gunicorn + Nginx setup, file path validation, URL mapping, and permission fixes.
Quick Fix / Quick Setup
Create a dedicated media directory, set correct permissions, and map /media/ to that directory in Nginx.
sudo mkdir -p /var/www/myapp/media
sudo chown -R www-data:www-data /var/www/myapp/media
sudo chmod -R 755 /var/www/myapp/media
# Nginx: map URL path to media directory
sudo tee /etc/nginx/sites-available/myapp >/dev/null <<'EOF'
server {
server_name example.com;
location /media/ {
alias /var/www/myapp/media/;
autoindex off;
}
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
}
EOF
sudo nginx -t && sudo systemctl reload nginx
Use alias, not root, for the /media/ location. The filesystem path must end with a trailing slash, and uploaded files must actually exist in that directory.
What’s Happening
In production, Gunicorn should not serve user-uploaded media files directly. Nginx should usually serve /media/ URLs from a directory on disk using an alias mapping. If the URL path, filesystem path, or permissions are wrong, uploads appear broken even if the app saved them correctly.
Step-by-Step Guide
- Confirm the upload directory used by Flask
Check your app config forMEDIA_ROOT,UPLOAD_FOLDER, or equivalent.pythonUPLOAD_FOLDER = "/var/www/myapp/media" - Confirm the media URL prefix used by the app
Uploaded file links should resolve under a stable prefix such as/media/.pythonimage_url = f"/media/{filename}" - Create the media directory if it does not existbash
sudo mkdir -p /var/www/myapp/media - Place a known file in the media directory for testingbash
sudo cp test.jpg /var/www/myapp/media/ - Set ownership so the app can write and Nginx can read
A common setup is to usewww-datafor both.bashsudo chown -R www-data:www-data /var/www/myapp/media - Set directory and file permissionsbash
sudo find /var/www/myapp/media -type d -exec chmod 755 {} \; sudo find /var/www/myapp/media -type f -exec chmod 644 {} \; - Configure Nginx to serve
/media/from disk
Add a dedicated location block.nginxlocation /media/ { alias /var/www/myapp/media/; autoindex off; } - Make sure trailing slashes match
If the location ends with/, the alias path should also end with/.
Correct:nginxlocation /media/ { alias /var/www/myapp/media/; } - Use a full Nginx server block if needednginx
server { listen 80; server_name example.com; location /media/ { alias /var/www/myapp/media/; autoindex off; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } } - Test and reload Nginx
sudo nginx -t
sudo systemctl reload nginx
- Request a known file directly
Test a file without going through app templates.
curl -I http://127.0.0.1/media/test.jpg
curl -I https://example.com/media/test.jpg
- Verify the app and Nginx point to the same location
If Flask saves to /srv/uploads but Nginx reads /var/www/myapp/media, media delivery will fail. Align both paths.
- Check generated URLs
If your app generates absolute URLs, confirm the hostname and scheme are correct. Media links should not point to localhost, an old domain, or an incorrect prefix like /uploads/ if Nginx serves /media/.
- If using containers, verify persistent storage
The media directory must be mounted to persistent storage and visible to the process serving files.
docker inspect <container_name> | grep -A 20 Mounts
- Remove route conflicts
If Flask uses send_from_directory in production while Nginx is also serving /media/, make sure routes do not conflict. In standard production setups, let Nginx serve media files directly.
- Retest with a new upload
Upload a new file, confirm it exists on disk, and request it using the generated /media/... URL.
Common Causes
- Wrong Nginx directive → Using
rootinstead ofaliasfor/media/builds the wrong filesystem path → Replace it with:nginxlocation /media/ { alias /var/www/myapp/media/; } - Wrong directory path → Flask saves uploads to one directory while Nginx reads another → Align
UPLOAD_FOLDERand the Nginxaliastarget. - Missing trailing slash →
location /media/withalias /path/mediacan resolve paths incorrectly → Use/media/and/path/media/together. - Permissions problem → Uploads exist but Nginx cannot read them, or Flask cannot write them → Fix owner and mode:bash
sudo chown -R www-data:www-data /var/www/myapp/media sudo find /var/www/myapp/media -type d -exec chmod 755 {} \; sudo find /var/www/myapp/media -type f -exec chmod 644 {} \; - Uploads not persisted → Files disappear after restart or deploy, especially with Docker → Mount a persistent volume for media storage.
- Broken URL generation → Templates reference
/uploads/while Nginx serves/media/→ Standardize the URL prefix. - Files never saved → The application failed to write uploads due to validation or write errors → Check Flask and Gunicorn logs.
- Location block conflict → Another Nginx rule overrides
/media/requests → Inspect the full loaded config withnginx -T. - SELinux or AppArmor restrictions → Access is denied despite normal Unix permissions → Adjust policy or move media to an allowed path.
- CDN or proxy cache issue → Old 404 responses are cached → Purge cache or test directly against origin.
Debugging Section
Check file existence first:
ls -lah /var/www/myapp/media
stat /var/www/myapp/media/test.jpg
namei -l /var/www/myapp/media/test.jpg
Test HTTP responses locally and publicly:
curl -I http://127.0.0.1/media/test.jpg
curl -I https://example.com/media/test.jpg
Validate the active Nginx configuration:
sudo nginx -t
sudo nginx -T | less
Reload after changes:
sudo systemctl reload nginx
Inspect Nginx logs:
sudo journalctl -u nginx -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/access.log
Inspect Gunicorn logs if uploads may not be saved correctly:
sudo journalctl -u gunicorn -n 100 --no-pager
If using Docker, verify mounts:
docker inspect <container_name> | grep -A 20 Mounts
What to look for:
404in access logs → wrong path mapping or missing file403→ permissions or security policy issueopen() failedin Nginx logs → wrong alias path or unreadable file- file missing on disk → Flask upload handling failed or storage is ephemeral
- requests hitting Flask instead of Nginx media location → route conflict or bad Nginx config
Checklist
- Uploaded files exist on disk in the expected media directory
- Flask saves uploads to the same directory Nginx serves
- Nginx has a
location /media/block withalias, notroot - The alias path ends with a trailing slash
- Directory ownership and permissions allow write by app and read by Nginx
-
nginx -tpasses without errors - Reloading Nginx applies the config successfully
- A known file opens directly from its
/media/URL - New uploads remain available after app restart or deploy
- If using containers, media storage is persistent and shared correctly
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Static and Media Files Production Setup
- Flask 404 on Static or Media Files
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Should Gunicorn serve uploaded media files directly?
A: No. In production, Nginx should normally serve uploaded files from disk.
Q: What Nginx directive should be used for /media/?
A: Use alias for a dedicated media URL prefix, not root.
Q: Why do uploads work locally but not on the server?
A: The development server may serve files automatically, but production requires explicit Nginx media mapping and correct permissions.
Q: Why are media files returning 404 after deploy?
A: The files may be stored in the wrong directory, deleted during deploy, or served from a mismatched URL prefix.
Q: How do I confirm the problem is Nginx and not Flask?
A: Check whether the file exists on disk, then request it directly via the /media/ URL and inspect Nginx logs.
Final Takeaway
Most broken Flask media delivery issues come from one of three problems: wrong path, wrong Nginx mapping, or wrong permissions. Validate where the app saves files, map that directory with an Nginx alias, and confirm the files exist and are readable. Once a direct /media/file request works, upload handling in production is usually fixed.