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.

Browser
Nginx/CDN
app/static directory or object storage

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

bash
# 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 static

Most 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/302 loops, 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:

bash
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.

bash
curl -I https://yourdomain.com/static/app.css
curl -vk https://yourdomain.com/static/app.css

What to look for:

  • 404: path mismatch or file missing
  • 403: permissions or server restriction
  • 301/302: redirect problem or wrong host/protocol
  • Content-Type: text/html for a .css or .js file: server is returning an error page instead of the asset
  • http:// asset on an https:// 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

bash
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

Docker deploy

bash
docker ps
docker exec -it <container> sh
find /app -type f | grep -E 'static|assets|css|js'
ls -lah /app/static

If 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:

bash
sudo nginx -t
sudo nginx -T
cat /etc/nginx/sites-enabled/*

Minimal correct example with alias:

nginx
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:

nginx
location /static/ {
    alias /var/www/app/static/;
}

Request:

txt
/static/app.css

Resolved file:

txt
/var/www/app/static/app.css

Using root:

nginx
location /static/ {
    root /var/www/app;
}

Request:

txt
/static/app.css

Resolved file:

txt
/var/www/app/static/app.css

Using the wrong one with the wrong directory structure creates 404s.

Bad example:

nginx
location /static/ {
    root /var/www/app/static/;
}

That may resolve to:

txt
/var/www/app/static/static/app.css

After fixing config:

bash
sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

5. 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:

bash
find /app -type f | grep -E 'index-.*\.js|app\..*\.css|assets'
ls -lah /app/dist
ls -lah /app/build

Docker multi-stage example:

dockerfile
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/dist

Common 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.

bash
stat /var/www/app/static/app.css
namei -l /var/www/app/static/app.css
ps aux | grep nginx

Typical fix:

bash
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:

bash
docker inspect <container> | grep -A 20 Mounts

If 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:

bash
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.log

Then 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 root instead of alias, 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_URL or 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.

bash
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:

  1. Copy one failing asset URL from devtools.
  2. curl -I the exact URL.
  3. Identify which service should serve it.
  4. Confirm the file exists in that runtime.
  5. Match URL path to Nginx or framework mapping.
  6. Check permissions.
  7. 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.

Aspectaliasroot 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 -I against 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


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.