Flask Static vs Media Files Explained
If you're trying to understand why CSS, JavaScript, images, or user uploads behave differently in Flask production, this guide shows you how to configure them correctly. The outcome is a working file layout and routing setup that avoids broken assets, missing uploads, and incorrect Nginx routing.
Quick Fix / Quick Setup
Use separate directories and URL paths immediately:
# Example production layout
sudo mkdir -p /var/www/myapp/static
sudo mkdir -p /var/www/myapp/media
sudo chown -R www-data:www-data /var/www/myapp/media
# app.py
from flask import Flask
app = Flask(__name__, static_folder='static', static_url_path='/static')
app.config['UPLOAD_FOLDER'] = '/var/www/myapp/media'
# /static/ -> app static files
location /static/ {
alias /var/www/myapp/static/;
}
# /media/ -> uploaded files
location /media/ {
alias /var/www/myapp/media/;
}
sudo nginx -t && sudo systemctl reload nginx
Use static for versioned app assets such as CSS, JS, and logos. Use media for files created or uploaded after deployment. Do not mix them into the same directory or URL path.
What’s Happening
Static files are application assets shipped with the release: CSS, JavaScript, fonts, and fixed images. Media files are runtime-generated or user-uploaded content: avatars, documents, reports, and attachments.
In production, Flask should not serve either type directly when Nginx is available. Nginx should map /static/ and /media/ to separate filesystem paths. Most production issues happen when both types share one folder, one URL prefix, or incorrect Nginx alias settings.
Step-by-Step Guide
- Reserve separate URL paths
Use:/static/for application assets/media/for uploaded or generated files
- Create separate filesystem directories
Example:bashsudo mkdir -p /var/www/myapp/static sudo mkdir -p /var/www/myapp/media - Keep media outside the release path
If your deployment replaces the app directory, uploaded files stored there will be lost. Use a persistent path:bash/var/www/myapp/media - Configure Flask static settings explicitlypython
from flask import Flask app = Flask(__name__, static_folder='static', static_url_path='/static') - Configure upload storage explicitlypython
app.config['UPLOAD_FOLDER'] = '/var/www/myapp/media' - Save uploaded files into the media directory
Example:pythonimport os from werkzeug.utils import secure_filename from flask import request file = request.files['file'] filename = secure_filename(file.filename) file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) - Do not save uploads inside app source directories
Avoid paths such as:bash/var/www/myapp/current/app/static/uploads
Use a persistent media path instead. - Add Nginx location blocks for direct file serving
Usealias, notproxy_pass, for static and media:nginxlocation /static/ { alias /var/www/myapp/static/; expires 30d; access_log off; } location /media/ { alias /var/www/myapp/media/; } - Use trailing slashes correctly
If the location ends with a slash, the alias should also end with a slash:nginxlocation /media/ { alias /var/www/myapp/media/; } - Set ownership and permissions
The app process must be able to write to media. Nginx must be able to read static and media:bashsudo chown -R www-data:www-data /var/www/myapp/media sudo chmod -R 755 /var/www/myapp/static sudo chmod -R 755 /var/www/myapp/media - Validate and reload Nginxbash
sudo nginx -t sudo systemctl reload nginx - Test both paths directlybash
curl -I http://localhost/static/app.css curl -I http://localhost/media/test.jpg
Expected result:200 OKfor known files. - Handle Docker persistence correctly
If using containers, keep static in the image or in a mounted path, and mount media as a persistent volume:yamlvolumes: - ./media:/var/www/myapp/media
Common Causes
- Static and media are stored in the same directory → deployments, cache rules, and permissions become inconsistent → split them into separate folders and URL prefixes.
- Uploads are saved inside the project release directory → files disappear on redeploy → move media to a persistent path outside the release.
- Nginx alias points to the wrong path → requests return 404 even though files exist → verify the alias target and trailing slash usage.
- The app writes uploads to one path while Nginx serves another → uploads succeed but files are not reachable → align
UPLOAD_FOLDERwith the Nginx media alias. - Permissions prevent writes to media → uploads fail or create empty paths → grant write access to the app user and read access to Nginx.
- Static files are proxied to Gunicorn instead of served by Nginx → poor performance or path confusion → add dedicated Nginx location blocks for
/static/and/media/. - Docker stores uploads in an ephemeral filesystem → files vanish after restart → mount a persistent volume for media.
Debugging Section
Check configuration, paths, and logs in this order.
Validate Nginx config
sudo nginx -t
Reload after changes
sudo systemctl reload nginx
Watch Nginx logs
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
What to look for:
404on/static/...or/media/...- permission-related errors
- wrong resolved paths
Verify directories exist
ls -lah /var/www/myapp/static
ls -lah /var/www/myapp/media
find /var/www/myapp/media -maxdepth 2 -type f | head
Check path traversal and permissions
namei -l /var/www/myapp/media
stat /var/www/myapp/media
What to look for:
- execute permission on parent directories
- write permission for the app user
- readable files for Nginx
Test direct file access
curl -I http://localhost/static/app.css
curl -I http://localhost/media/test.jpg
Inspect Flask config values
python -c "from app import app; print(app.static_folder); print(app.static_url_path); print(app.config.get('UPLOAD_FOLDER'))"
What to confirm:
static_folderpoints to the intended static directorystatic_url_pathis/staticUPLOAD_FOLDERmatches the Nginx/media/alias target
Checklist
- Static and media use different URL prefixes.
- Static and media use different filesystem directories.
- Flask
UPLOAD_FOLDERpoints to the media directory. - Nginx serves
/static/and/media/with correctaliaspaths. - The application process can write to media.
- Nginx can read static and media files.
- Uploaded files remain present after redeploy.
- Direct requests to known static and media files return
200.
Related Guides
- Flask Static and Media Files Production Setup
- Flask Static Files Not Loading in Production
- Flask 404 on Static or Media Files
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: What is the difference between static and media files in Flask?
A: Static files are shipped with the app release. Media files are uploaded or generated after deployment.
Q: Should uploaded files go into the static folder?
A: No. Uploaded files should use a separate media directory and URL path.
Q: Why does Nginx need separate locations for static and media?
A: They often have different paths, permissions, and caching behavior, so separate locations prevent routing mistakes.
Q: Can Flask serve static and media in production?
A: It can, but Nginx should serve them in production for better performance and simpler routing.
Q: Can media files be public?
A: Yes, if intended. If access must be restricted, serve them through authenticated application logic instead of a public Nginx alias.
Q: Why do my uploads disappear after container restart?
A: They are likely stored in the container filesystem instead of a persistent mounted volume.
Final Takeaway
Static files are deployment-time assets. Media files are runtime-created content. Keep them separated in both URL paths and filesystem directories, and let Nginx serve each from the correct location.