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

Flask Static Files Not Loading in Production

If your Flask CSS, JavaScript, images, or other static assets are not loading in production, this guide shows you how to identify the break point and fix it step-by-step. The goal is to make Nginx serve static files correctly and confirm your app generates the right URLs.

Quick Fix / Quick Setup

Use this first if your production stack is Flask + Gunicorn + Nginx.

bash
# 1) Verify your Flask app exposes the expected static path
python - <<'PY'
from app import app
print('static_folder=', app.static_folder)
print('static_url_path=', app.static_url_path)
PY

# 2) Confirm files exist on disk
ls -lah /var/www/myapp/static

# 3) Add or fix Nginx static mapping
sudo nano /etc/nginx/sites-available/myapp

Example Nginx server block:

nginx
server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /var/www/myapp/static/;
        expires 7d;
        add_header Cache-Control "public";
    }

    location / {
        proxy_pass http://unix:/run/gunicorn.sock;
        include proxy_params;
    }
}

Apply and test:

bash
# 4) Test and reload Nginx
sudo nginx -t && sudo systemctl reload nginx

# 5) Verify HTTP response
curl -I http://127.0.0.1/static/app.css
curl -I https://example.com/static/app.css

In most production setups, Nginx should serve static files directly. The fastest fix is usually correcting the Nginx location block, alias path, file permissions, or the Flask static_url_path.

What’s Happening

In production, Gunicorn usually handles dynamic Flask requests while Nginx serves static files directly. Static files fail when the requested URL does not match the Nginx mapping, the on-disk path is wrong, or Nginx cannot read the files. Another common failure is that Flask templates generate the wrong asset URL, so the browser requests a path that does not exist.

Step-by-Step Guide

  1. Check the failing asset URL in the browser
    Open browser dev tools and inspect the Network tab. Identify the exact asset path and HTTP status.
    Look for:
    • /static/... vs /assets/...
    • 404, 403, or unexpected redirects
    • wrong domain or protocol
  2. Verify the file exists on disk
    Confirm the static directory and target file are present on the server.
    bash
    ls -lah /var/www/myapp/static
    ls -lah /var/www/myapp/static/css/app.css
    find /var/www/myapp/static -maxdepth 3 -type f | head -50
    
  3. Confirm Flask generates the correct static URL
    In templates, use url_for() instead of hardcoded paths.
    Correct:
    jinja2
    <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
    

    Avoid:
    html
    <link rel="stylesheet" href="/css/app.css">
    
  4. Check Flask static settings
    Print the active static configuration.
    bash
    python - <<'PY'
    from app import app
    print(app.static_folder)
    print(app.static_url_path)
    PY
    

    Example Flask app:
    python
    from flask import Flask
    
    app = Flask(__name__, static_folder="static", static_url_path="/static")
    

    If static_url_path is /assets, Nginx must serve /assets/, not /static/.
  5. Inspect the Nginx static mapping
    Open the active site config:
    bash
    sudo nano /etc/nginx/sites-available/myapp
    

    Recommended setup:
    nginx
    server {
        listen 80;
        server_name example.com;
    
        location /static/ {
            alias /var/www/myapp/static/;
            expires 7d;
            add_header Cache-Control "public";
        }
    
        location / {
            proxy_pass http://unix:/run/gunicorn.sock;
            include proxy_params;
        }
    }
    

    Critical details:
    • location /static/ must match the requested URL prefix
    • alias should point to the real deployed directory
    • keep the trailing slash in alias /var/www/myapp/static/;
  6. Use alias carefully
    For Flask static folders, alias is usually safer than root.
    Good:
    nginx
    location /static/ {
        alias /var/www/myapp/static/;
    }
    

    If using root, verify the final resolved path is correct.
    Example:
    nginx
    location /static/ {
        root /var/www/myapp;
    }
    

    That makes /static/app.css resolve to:
    text
    /var/www/myapp/static/app.css
    
  7. Validate the Nginx configuration
    Test before reload.
    bash
    sudo nginx -t
    

    If needed, print the loaded config:
    bash
    sudo nginx -T | sed -n '/server_name example.com/,/}/p'
    
  8. Reload Nginx
    After fixing the config:
    bash
    sudo systemctl reload nginx
    sudo systemctl status nginx
    
  9. Verify file and directory permissions
    Nginx must be able to traverse parent directories and read files.
    Check effective path permissions:
    bash
    namei -l /var/www/myapp/static/css/app.css
    stat /var/www/myapp/static/css/app.css
    ps aux | grep nginx
    

    Typical safe permissions:
    bash
    sudo chmod 755 /var/www
    sudo chmod 755 /var/www/myapp
    sudo chmod -R 755 /var/www/myapp/static
    

    If ownership is incorrect:
    bash
    sudo chown -R root:root /var/www/myapp/static
    
  10. Confirm requests are not being proxied to Gunicorn
    If Gunicorn receives /static/ requests, Nginx routing is wrong.
    Check whether the generic proxy block is catching static requests before the static block. Your Nginx config should include a dedicated location /static/ block.
  11. Check Docker or multi-container setups
    If using Docker, make sure the static directory exists in the container actually serving the files.
    Example checks:
    bash
    docker ps
    docker exec -it nginx-container ls -lah /var/www/myapp/static
    docker exec -it nginx-container ls -lah /var/www/myapp/static/css/app.css
    

    If Nginx runs in a separate container, mount the same static directory into that container.
  12. Check direct HTTP responses
    Test from localhost and through the public domain.
    bash
    curl -I http://127.0.0.1/static/app.css
    curl -I https://example.com/static/app.css
    

    Expected result:
    • 200 OK or 304 Not Modified
  13. Inspect rendered HTML
    Confirm the app is emitting the URL you expect.
    bash
    curl -s https://example.com | grep -o '/static/[^" ]*' | head
    

    If the HTML points to /assets/... but Nginx only serves /static/..., align them.
  14. Clear stale browser references
    If the config is fixed but the browser still requests old filenames, clear cache or use a cache-busting query string.
    Example:
    jinja2
    <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}?v=20260421">
    
  15. Check case sensitivity, symlinks, and security controls
    If only some files fail:
    • verify exact filename case
    • verify symlink targets
    • check SELinux or AppArmor if enabled
    • confirm the file was actually deployed

Common Causes

  • Incorrect Nginx alias path → Nginx points /static/ to the wrong directory → update alias to the real deployed static folder and reload Nginx.
  • Missing trailing slash in alias → Nginx resolves file paths incorrectly → use alias /var/www/myapp/static/; for location /static/.
  • Templates use hardcoded asset paths → browser requests the wrong URL → use url_for('static', filename='...').
  • Static files were not deployed → files exist locally but not on the server → sync or copy the static directory during deployment.
  • Wrong Flask static_url_path → app generates /assets/... while Nginx serves /static/... → align Flask and Nginx URL paths.
  • Permissions blocked → Nginx user cannot read files or traverse parent directories → fix ownership and chmod on directories and files.
  • Docker volume mismatch → static files exist in one container but not in the serving container → mount the same static directory where Nginx serves from.
  • Nginx config not loaded → you edited one file but another server block is active → verify active config with nginx -T.
  • Static requests are proxied to Gunicorn → no dedicated Nginx location block handles /static/ → add the static location before the generic proxy location.
  • Browser cache serves stale references → HTML points to old filenames after deploy → clear cache or use versioned static assets.

Debugging Section

Check these commands in order:

bash
ls -lah /var/www/myapp/static
find /var/www/myapp/static -maxdepth 3 -type f | head -50

python - <<'PY'
from app import app
print(app.static_folder)
print(app.static_url_path)
PY

sudo nginx -t
sudo nginx -T | sed -n '/server_name example.com/,/}/p'

curl -I http://127.0.0.1/static/app.css
curl -I https://example.com/static/app.css
curl -s https://example.com | grep -o '/static/[^" ]*' | head

sudo tail -n 100 /var/log/nginx/access.log
sudo tail -n 100 /var/log/nginx/error.log

namei -l /var/www/myapp/static/css/app.css
stat /var/www/myapp/static/css/app.css

ps aux | grep nginx
sudo systemctl status nginx

What to look for:

  • Access log
    • confirms whether the request reaches Nginx
    • shows actual status code returned
  • Error log
    • permission denied
    • file not found
    • bad alias mapping
    • wrong virtual host handling the request
  • Rendered HTML
    • incorrect /static/ path
    • wrong domain
    • mixed-content issues on HTTPS
  • nginx -T output
    • confirms the expected server block is loaded
    • reveals duplicate or conflicting server_name blocks
  • Gunicorn behavior
    • if /static/ requests hit Gunicorn, reverse proxy routing is wrong

Checklist

  • Static files exist in the deployed filesystem path.
  • Flask templates use url_for('static', filename=...).
  • Flask static_url_path matches the public URL prefix.
  • Nginx has a location /static/ block.
  • The Nginx alias or root path matches the real static directory.
  • nginx -t passes without errors.
  • Nginx has been reloaded after config changes.
  • File and directory permissions allow Nginx to read static assets.
  • Direct requests to /static/... return 200 or 304.
  • Browser dev tools no longer show failed CSS, JS, or image requests.

FAQ

Q: Should I let Flask serve static files in production?
A: Usually no. Use Nginx to serve static files directly and keep Gunicorn for app requests.

Q: What status code usually indicates this problem?
A: Most often 404 or 403. A 404 usually means wrong path or missing files. A 403 usually means permissions.

Q: Why do assets load on localhost but fail on the live domain?
A: Your local development server may serve static files automatically, while production depends on Nginx or container routing.

Q: Can a bad DNS or HTTPS setup cause missing static files?
A: Yes. If assets are requested from the wrong domain, blocked by mixed-content rules, or routed to the wrong vhost, they may fail even when files exist.

Final Takeaway

Static file failures in Flask production usually come from one of four issues: wrong URL, wrong Nginx mapping, missing files, or read permissions. Validate the request path, file location, and active Nginx config in that order to resolve the issue quickly.