Static Files Not Loading
The essential playbook for implementing static files not loading in your SaaS.
Use this page when your app renders HTML but CSS, JavaScript, fonts, or images return 404, 403, mixed-content, or incorrect MIME type errors. The goal is to verify where static files live, which service should serve them, and whether your app generates the correct URLs in production.
request flow diagram showing Browser -> Nginx/CDN -> app/static directory or object storage, with failure points at URL generation, server mapping, filesystem, permissions, and cache.
Quick Fix / Quick Setup
# 1) Find the broken asset URL in browser devtools, then test it directly
curl -I https://yourdomain.com/static/app.css
# 2) Verify the file exists on disk inside the running server/container
ls -lah /var/www/app/static/
find /app -type f | grep 'app.css'
# 3) If using Nginx, confirm static mapping
sudo nginx -T | sed -n '/server_name yourdomain.com/,/}/p'
# Example Nginx block
location /static/ {
alias /var/www/app/static/;
access_log off;
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
# 4) Reload Nginx after fixing config
sudo nginx -t && sudo systemctl reload nginx
# 5) If using Docker, confirm assets are copied into the image/container
docker exec -it <container> sh
find /app -type f | grep staticMost production static-file issues come from one of five problems: wrong URL path, file missing from deploy artifact, bad Nginx alias/root usage, framework static configuration mismatch, or container volume/build mistakes.
What’s happening
- The HTML response is working, but requests for
/static/,/assets/,/media/, or built frontend files fail. - The browser usually shows
404 Not Found,403 Forbidden,301/302loops, mixed-content warnings, or MIME type errors. - In production, static files are often served by Nginx, a CDN, object storage, or a framework-specific static server. If that layer is misconfigured, app pages load without styling or scripts.
- A common failure pattern is that local development works because the framework serves assets directly, while production requires an explicit static-file setup.
Step-by-step implementation
1. Confirm the exact failing asset URL
Do not debug from templates or assumptions. Open browser devtools and copy the exact failing URL.
Examples:
/static/app.css/assets/index-abc123.js/media/logo.png
Check from the page HTML if needed:
curl -s https://yourdomain.com | grep -E 'static|assets|css|js'2. Test the asset directly
Inspect status code, content type, redirects, and TLS behavior.
curl -I https://yourdomain.com/static/app.css
curl -vk https://yourdomain.com/static/app.cssWhat to look for:
404: path mismatch or file missing403: permissions or server restriction301/302: redirect problem or wrong host/protocolContent-Type: text/htmlfor a.cssor.jsfile: server is returning an error page instead of the assethttp://asset on anhttps://page: mixed-content issue
3. Verify the file exists in the active runtime
Check the real deployment target, not your local machine.
VPS or direct server deploy
ls -lah /var/www/app/static/
find /var/www/app -type f | grep -E 'static|assets|css|js'
stat /var/www/app/static/app.css
namei -l /var/www/app/static/app.cssDocker deploy
docker ps
docker exec -it <container> sh
find /app -type f | grep -E 'static|assets|css|js'
ls -lah /app/staticIf the file does not exist inside the container or active release, the issue is in the build or deploy process.
4. Verify Nginx mapping
Print the real merged config:
sudo nginx -t
sudo nginx -T
cat /etc/nginx/sites-enabled/*Minimal correct example with alias:
server {
listen 80;
server_name yourdomain.com;
location /static/ {
alias /var/www/app/static/;
access_log off;
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
location / {
proxy_pass http://127.0.0.1:8000;
include proxy_params;
}
}alias vs root
This is a common failure point.
Using alias:
location /static/ {
alias /var/www/app/static/;
}Request:
/static/app.cssResolved file:
/var/www/app/static/app.cssUsing root:
location /static/ {
root /var/www/app;
}Request:
/static/app.cssResolved file:
/var/www/app/static/app.cssUsing the wrong one with the wrong directory structure creates 404s.
Bad example:
location /static/ {
root /var/www/app/static/;
}That may resolve to:
/var/www/app/static/static/app.cssAfter fixing config:
sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log5. Validate framework static configuration
Your app should generate URLs that match what the web server serves.
Examples to verify:
- Public path is
/static/or/assets/ - Templates are not hardcoding old file paths
- Environment-specific asset base URL is correct
- App is not routing
/static/through application handlers in production
If using a Python framework behind Nginx, Nginx should usually serve static files directly, while the app generates the correct URLs.
Also verify your broader deploy setup matches your server config in Deploy SaaS with Nginx + Gunicorn and your runtime image layout in Docker Production Setup for SaaS.
6. Confirm frontend build output exists
If using Vite, Webpack, or another bundler:
- confirm the build step ran
- confirm output directory matches deploy config
- confirm the built files were copied into the final release
- confirm the app references the current hashed filenames
Example checks:
find /app -type f | grep -E 'index-.*\.js|app\..*\.css|assets'
ls -lah /app/dist
ls -lah /app/buildDocker multi-stage example:
FROM node:20 AS frontend-build
WORKDIR /src
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM python:3.12-slim
WORKDIR /app
COPY . .
COPY --from=frontend-build /src/dist /app/static/distCommon failure: assets are built in one stage and never copied into the final image.
7. Check permissions
Static files need read permission, and parent directories need execute permission for traversal.
stat /var/www/app/static/app.css
namei -l /var/www/app/static/app.css
ps aux | grep nginxTypical fix:
sudo chown -R www-data:www-data /var/www/app/static
sudo find /var/www/app/static -type d -exec chmod 755 {} \;
sudo find /var/www/app/static -type f -exec chmod 644 {} \;If directory traversal is blocked, Nginx may return 403 even when the file exists.
8. Check Docker volume masking
A mounted volume can hide files that were present in the image.
Example problem:
- image contains
/app/static/app.css - runtime volume mounts
/app - mounted path does not contain static files
- file appears missing in the running container
Inspect mounts:
docker inspect <container> | grep -A 20 MountsIf you mount the project directory in production, confirm it includes the built assets.
9. Check cache, CDN, and stale hashed filenames
Common release failure:
- new deploy generates
app.456def.css - old HTML or CDN still references
app.123abc.css - users get 404 for old hashed files
Checks:
- hard-refresh in browser
- test in incognito
- purge CDN if used
- keep previous assets available during rollout if pages can be cached
- avoid serving HTML with stale asset references
If you use external storage or separate static/media hosting, verify the setup in Static and Media File Handling.
10. Re-test end-to-end
After changes:
curl -I https://yourdomain.com/static/app.css
curl -vk https://yourdomain.com/static/app.css
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.logThen re-open the page in:
- hard refresh
- incognito window
- another device if CDN is involved
Common causes
- Nginx location block points to the wrong directory.
- Using
rootinstead ofalias, or alias/root path mismatch. - Static files were not copied into the deployment directory or Docker image.
- Frontend build step did not run, failed, or output to a different folder than expected.
- The HTML references old hashed asset filenames after deployment.
STATIC_URLor asset base path is wrong for the production domain or subpath.- File permissions prevent Nginx or the app user from reading the files.
- A Docker volume overrides the image filesystem and hides built assets.
- HTTP asset URLs are blocked on an HTTPS page as mixed content.
- CDN or reverse-proxy cache is serving stale references or stale 404s.
- Object storage bucket or CDN origin is misconfigured.
- Application routes are catching
/static/requests before the static server handles them.
Debugging tips
Use these commands directly.
curl -I https://yourdomain.com/static/app.css
curl -vk https://yourdomain.com/static/app.css
sudo nginx -t
sudo nginx -T
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log
ls -lah /var/www/app/static/
find /var/www/app -type f | grep -E 'static|assets|css|js'
stat /var/www/app/static/app.css
namei -l /var/www/app/static/app.css
ps aux | grep nginx
docker ps
docker exec -it <container> sh
find /app -type f | grep -E 'static|assets|css|js'
cat /etc/nginx/sites-enabled/*
curl -s https://yourdomain.com | grep -E 'static|assets'Fast isolation workflow:
- Copy one failing asset URL from devtools.
curl -Ithe exact URL.- Identify which service should serve it.
- Confirm the file exists in that runtime.
- Match URL path to Nginx or framework mapping.
- Check permissions.
- Check cache and old hashed filenames.
If the issue is part of a broader production incident, use Debugging Production Issues and error capture in Error Tracking with Sentry.
| Aspect | alias | root path resolution in Nginx |
|---|---|---|
| — | — | — |
side-by-side diagram for alias vs root path resolution in Nginx.
Checklist
- ✓ A failing asset URL has been identified from browser devtools.
- ✓
curl -Iagainst the asset URL returns the expected status code and content-type. - ✓ The file exists in the active deployment or running container.
- ✓ Nginx location and filesystem mapping are correct.
- ✓ Framework static settings or mounted static directories match the public URL path.
- ✓ Frontend build artifacts were generated and copied during deploy.
- ✓ Permissions allow the web server to read the files.
- ✓ CDN/cache invalidation is handled after deployment changes.
- ✓ Page loads correctly after hard refresh and incognito test.
For final release validation, include this in your broader rollout process with SaaS Production Checklist.
Related guides
- Deploy SaaS with Nginx + Gunicorn
- Docker Production Setup for SaaS
- Static and Media File Handling
- Media Uploads Not Working
- SaaS Production Checklist
FAQ
Why are only CSS and JS failing but HTML works?
Your app route is responding correctly, but the static-serving layer is misconfigured or the referenced asset files do not exist in the active release.
How do I know whether Nginx or my app should serve static files?
In a typical production setup with Gunicorn, Nginx should serve static files directly. The app should generate correct URLs, but not usually handle static asset delivery.
What is the difference between alias and root in Nginx?
root appends the full request URI after the root path. alias replaces the matched location prefix with the alias path. Using the wrong one often produces 404s.
Why do assets break after a new deployment?
Hashed build files changed, but the HTML, cache, or CDN still references old filenames. This is common with frontend build pipelines and cached pages.
Can this be caused by file permissions?
Yes. If the web server user cannot traverse parent directories or read the file, requests may return 403 or behave like missing files depending on configuration.
Why do static files work locally but not in production?
Local development often uses the framework dev server, while production relies on Nginx, Docker image contents, or a separate asset pipeline.
What is the most common Nginx mistake?
Using root where alias is needed, or pointing alias to the wrong directory without the expected trailing slash behavior.
Why do I get a MIME type error for a JS or CSS file?
The server is often returning an HTML error page or redirect instead of the asset file.
Can Docker cause missing static files?
Yes. Files may never be copied into the final image, or a mounted volume may hide files that were present at build time.
Should the app server serve static files in production?
Usually no for a typical small SaaS behind Nginx. Let Nginx or object storage serve them directly.
Final takeaway
Static-file failures are usually path, mapping, or deploy-artifact problems, not application logic bugs.
Debug one asset URL from request path to filesystem path to isolate the issue quickly.
Once fixed, add deployment checks so missing assets fail the release before users see a broken UI.