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

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:

bash
# 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
python
# app.py
from flask import Flask

app = Flask(__name__, static_folder='static', static_url_path='/static')
app.config['UPLOAD_FOLDER'] = '/var/www/myapp/media'
nginx
# /static/ -> app static files
location /static/ {
    alias /var/www/myapp/static/;
}

# /media/ -> uploaded files
location /media/ {
    alias /var/www/myapp/media/;
}
bash
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

  1. Reserve separate URL paths
    Use:
    • /static/ for application assets
    • /media/ for uploaded or generated files
  2. Create separate filesystem directories
    Example:
    bash
    sudo mkdir -p /var/www/myapp/static
    sudo mkdir -p /var/www/myapp/media
    
  3. 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
    
  4. Configure Flask static settings explicitly
    python
    from flask import Flask
    
    app = Flask(__name__, static_folder='static', static_url_path='/static')
    
  5. Configure upload storage explicitly
    python
    app.config['UPLOAD_FOLDER'] = '/var/www/myapp/media'
    
  6. Save uploaded files into the media directory
    Example:
    python
    import 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))
    
  7. 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.
  8. Add Nginx location blocks for direct file serving
    Use alias, not proxy_pass, for static and media:
    nginx
    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
        access_log off;
    }
    
    location /media/ {
        alias /var/www/myapp/media/;
    }
    
  9. Use trailing slashes correctly
    If the location ends with a slash, the alias should also end with a slash:
    nginx
    location /media/ {
        alias /var/www/myapp/media/;
    }
    
  10. Set ownership and permissions
    The app process must be able to write to media. Nginx must be able to read static and media:
    bash
    sudo 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
    
  11. Validate and reload Nginx
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    
  12. Test both paths directly
    bash
    curl -I http://localhost/static/app.css
    curl -I http://localhost/media/test.jpg
    

    Expected result: 200 OK for known files.
  13. Handle Docker persistence correctly
    If using containers, keep static in the image or in a mounted path, and mount media as a persistent volume:
    yaml
    volumes:
      - ./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_FOLDER with 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

bash
sudo nginx -t

Reload after changes

bash
sudo systemctl reload nginx

Watch Nginx logs

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

What to look for:

  • 404 on /static/... or /media/...
  • permission-related errors
  • wrong resolved paths

Verify directories exist

bash
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

bash
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

bash
curl -I http://localhost/static/app.css
curl -I http://localhost/media/test.jpg

Inspect Flask config values

bash
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_folder points to the intended static directory
  • static_url_path is /static
  • UPLOAD_FOLDER matches the Nginx /media/ alias target

Checklist

  • Static and media use different URL prefixes.
  • Static and media use different filesystem directories.
  • Flask UPLOAD_FOLDER points to the media directory.
  • Nginx serves /static/ and /media/ with correct alias paths.
  • 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.

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.