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.
# 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:
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:
# 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
- 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
- Verify the file exists on disk
Confirm the static directory and target file are present on the server.bashls -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 - Confirm Flask generates the correct static URL
In templates, useurl_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"> - Check Flask static settings
Print the active static configuration.bashpython - <<'PY' from app import app print(app.static_folder) print(app.static_url_path) PY
Example Flask app:pythonfrom flask import Flask app = Flask(__name__, static_folder="static", static_url_path="/static")
Ifstatic_url_pathis/assets, Nginx must serve/assets/, not/static/. - Inspect the Nginx static mapping
Open the active site config:bashsudo nano /etc/nginx/sites-available/myapp
Recommended setup:nginxserver { 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 prefixaliasshould point to the real deployed directory- keep the trailing slash in
alias /var/www/myapp/static/;
- Use
aliascarefully
For Flask static folders,aliasis usually safer thanroot.
Good:nginxlocation /static/ { alias /var/www/myapp/static/; }
If usingroot, verify the final resolved path is correct.
Example:nginxlocation /static/ { root /var/www/myapp; }
That makes/static/app.cssresolve to:text/var/www/myapp/static/app.css - Validate the Nginx configuration
Test before reload.bashsudo nginx -t
If needed, print the loaded config:bashsudo nginx -T | sed -n '/server_name example.com/,/}/p' - Reload Nginx
After fixing the config:bashsudo systemctl reload nginx sudo systemctl status nginx - Verify file and directory permissions
Nginx must be able to traverse parent directories and read files.
Check effective path permissions:bashnamei -l /var/www/myapp/static/css/app.css stat /var/www/myapp/static/css/app.css ps aux | grep nginx
Typical safe permissions:bashsudo chmod 755 /var/www sudo chmod 755 /var/www/myapp sudo chmod -R 755 /var/www/myapp/static
If ownership is incorrect:bashsudo chown -R root:root /var/www/myapp/static - 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 dedicatedlocation /static/block. - Check Docker or multi-container setups
If using Docker, make sure the static directory exists in the container actually serving the files.
Example checks:bashdocker 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. - Check direct HTTP responses
Test from localhost and through the public domain.bashcurl -I http://127.0.0.1/static/app.css curl -I https://example.com/static/app.css
Expected result:200 OKor304 Not Modified
- Inspect rendered HTML
Confirm the app is emitting the URL you expect.bashcurl -s https://example.com | grep -o '/static/[^" ]*' | head
If the HTML points to/assets/...but Nginx only serves/static/..., align them. - 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"> - 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 → updatealiasto the real deployed static folder and reload Nginx. - Missing trailing slash in alias → Nginx resolves file paths incorrectly → use
alias /var/www/myapp/static/;forlocation /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
chmodon 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:
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
aliasmapping - wrong virtual host handling the request
- Rendered HTML
- incorrect
/static/path - wrong domain
- mixed-content issues on HTTPS
- incorrect
nginx -Toutput- confirms the expected server block is loaded
- reveals duplicate or conflicting
server_nameblocks
- Gunicorn behavior
- if
/static/requests hit Gunicorn, reverse proxy routing is wrong
- if
Checklist
- Static files exist in the deployed filesystem path.
- Flask templates use
url_for('static', filename=...). - Flask
static_url_pathmatches the public URL prefix. - Nginx has a
location /static/block. - The Nginx
aliasorrootpath matches the real static directory. -
nginx -tpasses without errors. - Nginx has been reloaded after config changes.
- File and directory permissions allow Nginx to read static assets.
- Direct requests to
/static/...return200or304. - Browser dev tools no longer show failed CSS, JS, or image requests.
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 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.