Flask Static and Media Files Production Setup
If you're trying to serve Flask static assets and uploaded media files correctly in production, this guide shows you how to set them up step-by-step. The goal is to let Nginx serve files directly, keep Gunicorn focused on the app, and prevent missing asset or upload errors.
Quick Fix / Quick Setup
Create dedicated static and media directories, map them in Nginx with alias, and keep application traffic proxied to Gunicorn.
sudo mkdir -p /var/www/myapp/static /var/www/myapp/media
sudo chown -R www-data:www-data /var/www/myapp
# Example Nginx locations
sudo tee /etc/nginx/sites-available/myapp >/dev/null <<'EOF'
server {
listen 80;
server_name example.com;
location /static/ {
alias /var/www/myapp/static/;
access_log off;
expires 7d;
}
location /media/ {
alias /var/www/myapp/media/;
access_log off;
expires 1d;
}
location / {
proxy_pass http://unix:/run/gunicorn.sock;
include proxy_params;
}
}
EOF
sudo nginx -t && sudo systemctl reload nginx
Use alias with a trailing slash, make sure the directories exist, and point Flask-generated URLs to /static/... and /media/....
What’s Happening
In production, Nginx should serve static files and uploaded media files directly instead of routing those requests through Gunicorn.
- Static files are application assets such as CSS, JavaScript, fonts, and images bundled with the app.
- Media files are user-generated uploads that must be stored in a writable directory and exposed through a dedicated URL path.
- Broken static or media delivery usually comes from wrong paths, wrong Nginx
aliasusage, missing files, or permission problems.
Step-by-Step Guide
- Define stable filesystem paths for static and media files.
Use directories outside rotating release paths.bashsudo mkdir -p /var/www/myapp/static /var/www/myapp/media - Set ownership and permissions.
Nginx must be able to read both directories. Your app process must be able to write uploads to the media directory.bashsudo chown -R www-data:www-data /var/www/myapp sudo find /var/www/myapp -type d -exec chmod 755 {} \; sudo find /var/www/myapp -type f -exec chmod 644 {} \; - Configure Flask path settings.
In Flask, keep the URL paths and disk paths explicit.pythonimport os STATIC_URL = "/static" MEDIA_URL = "/media" STATIC_ROOT = "/var/www/myapp/static" MEDIA_ROOT = "/var/www/myapp/media" app.config["UPLOAD_FOLDER"] = MEDIA_ROOT
If you generate media URLs in templates or responses, make sure they resolve under/media/.... - Copy application static assets into the production static directory.
If your project stores assets inapp/static/, sync them during deployment.bashrsync -av ./app/static/ /var/www/myapp/static/ - Save uploaded files into the media directory.
Example upload handling:pythonimport os from flask import request from werkzeug.utils import secure_filename app.config["UPLOAD_FOLDER"] = "/var/www/myapp/media" @app.post("/upload") def upload_file(): file = request.files["file"] filename = secure_filename(file.filename) file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename)) return {"filename": filename}, 201 - Update template and application URLs.
Static files should resolve under/static/.... Uploaded files should resolve under/media/....
Example:html<link rel="stylesheet" href="/static/css/app.css"> <img src="/media/example.jpg" alt=""> - Configure Nginx to serve static and media files directly.
Usealias, notproxy_pass, for these paths.nginxserver { listen 80; server_name example.com; location /static/ { alias /var/www/myapp/static/; access_log off; expires 7d; add_header Cache-Control "public"; } location /media/ { alias /var/www/myapp/media/; access_log off; expires 1d; add_header Cache-Control "public"; } location / { proxy_pass http://unix:/run/gunicorn.sock; include proxy_params; } } - Enable the Nginx site if needed.bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp - Validate and reload Nginx.bash
sudo nginx -t sudo systemctl reload nginx - Test static file serving directly.
echo 'ok' | sudo tee /var/www/myapp/static/health.txt
curl -I http://127.0.0.1/static/health.txt
Expected result: HTTP/1.1 200 OK
- Test media file serving directly.
echo 'upload-test' | sudo tee /var/www/myapp/media/sample.txt
curl -I http://127.0.0.1/media/sample.txt
Expected result: HTTP/1.1 200 OK
- Check SELinux or service restrictions if applicable.
On hardened systems, Nginx may still fail even when Unix permissions look correct. Verify read access for Nginx and write access for the app service to the media directory.
- Add static sync and media preservation to your deploy process.
Static files should be copied on every deploy. Media files should remain in persistent storage and not be replaced by a new release.
- Use persistent storage for containers.
If you deploy with containers, mount a volume for uploads.
volumes:
- /var/www/myapp/media:/app/media
Common Causes
- Wrong Nginx directive → Using
rootinstead ofaliasfor nested paths can map requests to the wrong directory → Usealias /full/path/;insidelocation /static/andlocation /media/. - Missing trailing slash →
aliaswithout the expected trailing slash can produce bad path resolution → Keep both thelocationandaliasforms consistent. - Files were never copied → Static assets exist in the repo but not in the production directory → Sync or collect them during deployment.
- Uploads saved to the wrong directory → Flask writes files somewhere other than the Nginx-exposed media path → Set
UPLOAD_FOLDERto the production media directory. - Permission denied → Nginx cannot read files or the app cannot write uploads → Fix ownership and mode bits for the service users.
- Requests hitting Gunicorn instead of Nginx static locations → Nginx location blocks are missing or shadowed by another rule → Put explicit
/static/and/media/locations in the server block. - URL mismatch → App generates
/uploads/...while Nginx serves/media/...→ Make the Flask URL path and Nginx location path match exactly. - Ephemeral deploy path → Static or media files are stored inside a release directory that gets replaced → Move them to stable paths under
/var/wwwor mounted storage.
Debugging Section
Check configuration, files, permissions, and request routing.
Nginx config validation
sudo nginx -t
sudo nginx -T | less
What to look for:
- syntax errors
- duplicate server blocks
- missing
/static/or/media/locations - wrong
aliaspaths
Nginx logs
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
What to look for:
404for files that do not exist on disk403caused by permissions- requests to
/static/or/media/being handled unexpectedly
File existence and path traversal
ls -lah /var/www/myapp/static
ls -lah /var/www/myapp/media
namei -l /var/www/myapp/static/health.txt
What to look for:
- missing files
- unreadable parent directories
- incorrect ownership on
/var,/var/www, or app subdirectories
Local request tests
curl -I http://127.0.0.1/static/health.txt
curl -I http://127.0.0.1/media/sample.txt
What to look for:
200 OKmeans Nginx can resolve and serve the file404means the URL-to-filesystem mapping is wrong or the file is missing403means permissions are blocking access
Gunicorn logs
sudo journalctl -u gunicorn -n 100 --no-pager
What to look for:
- upload write failures
- path misconfiguration in Flask
- attempts to handle static URLs in the application instead of Nginx
Checklist
-
/static/is mapped in Nginx withaliasto the correct absolute directory. -
/media/is mapped in Nginx withaliasto the correct absolute directory. - Static files exist in the production static directory.
- Uploaded files are written to the production media directory.
- Nginx can read both static and media directories.
- The app process can write to the media directory.
-
nginx -tpasses without errors. - A direct request to
/static/health.txtreturns200. - A direct request to a sample file under
/media/returns200. - Static and media paths are preserved across deployments.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Static Files Not Loading in Production
- Flask Media Files Not Serving (Uploads Broken)
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Should Flask serve static files in production?
A: No. Let Nginx serve static and media files directly for better performance and simpler routing.
Q: What is the difference between static and media files?
A: Static files are app assets shipped with the codebase. Media files are user uploads created after deployment.
Q: Should media files live inside the app release directory?
A: Usually no. Store them in a stable persistent path outside rotating release folders.
Q: Why are my uploads saving but not loading?
A: The app write path and Nginx read path are usually different, or Nginx lacks permission to read the files.
Q: Should Nginx or Gunicorn serve static files?
A: Nginx should serve static and media files directly in production.
Q: What path should uploaded files use?
A: Use a persistent writable directory such as /var/www/myapp/media, not a temporary release path.
Q: Why do static files work locally but fail in production?
A: Local Flask development serving can hide missing Nginx mappings, missing copied files, or production permission issues.
Q: Should static and media use the same directory?
A: No. Keep them separate so app assets and user uploads are managed independently.
Final Takeaway
Production Flask setups should let Nginx serve /static/ and /media/ directly from stable filesystem paths. If the URL paths, disk paths, and permissions all match, static assets and uploads will load reliably.