Troubleshooting
#django
#nginx
#whitenoise

Django Static Files Not Loading in Production: Fix Guide

A common production failure is that your Django app loads, but CSS, JavaScript, or Django admin assets are missing.

Problem statement

A common production failure is that your Django app loads, but CSS, JavaScript, or Django admin assets are missing. You may see unstyled admin pages, frontend CSS returning 404, or browser console errors for /static/... URLs after deployment.

This guide is for static files only: app CSS, JS, build artifacts, and Django admin assets. It is not about media files such as user uploads.

This usually appears in one of these production setups:

  • Django + Gunicorn + Nginx
  • Django + WhiteNoise
  • Dockerized Django behind Nginx or Caddy

In development, Django serves static files for you. In production, it does not unless you configure an explicit static serving path.

Quick answer

If Django static files are not loading in production, the fix is usually one of these:

  1. Set a valid STATIC_ROOT
  2. Run collectstatic
  3. Make sure /static/ is mapped correctly in Nginx or Caddy, or configure WhiteNoise correctly
  4. Verify the process serving files can read the directory
  5. If using Docker, confirm static files are actually present in the image or mounted volume

Start by checking the exact failing asset URL, then verify Django settings, then confirm the files exist on disk, then verify the serving layer.

Step-by-step solution

1) Confirm this is a static files issue

Typical symptoms:

  • Django admin loads without CSS
  • App CSS or JS returns 404 or 403
  • Static files worked with DEBUG=True but fail with DEBUG=False

Check the browser dev tools Network tab and note:

  • exact asset URL, for example /static/admin/css/base.css
  • response code: 404, 403, 500
  • content type, especially for CSS and JS

Also confirm this is not a media file problem. Static files are deployment artifacts. Media files are user uploads and are usually stored separately.

Verification check:

curl -I https://example.com/static/admin/css/base.css

You want to see 200 OK and an appropriate content type such as text/css.

2) Verify Django static settings

Check your production settings first.

# settings.py
DEBUG = False

INSTALLED_APPS = [
    # ...
    "django.contrib.staticfiles",
]

STATIC_URL = "/static/"
STATIC_ROOT = "/srv/app/staticfiles"

# Optional: source asset directories used before collectstatic
STATICFILES_DIRS = [
    BASE_DIR / "static",
]

What to verify:

  • DEBUG = False in production
  • django.contrib.staticfiles is enabled
  • STATIC_URL matches the URL prefix your proxy or app uses
  • STATIC_ROOT is a real destination directory for collected files
  • STATIC_ROOT is not the same as a source directory in STATICFILES_DIRS

Use findstatic to confirm Django can locate files before collection:

python manage.py findstatic admin/css/base.css

If this returns nothing, the file is not being found by Django’s staticfiles system.

If you use multiple settings modules, confirm which one is active for the release:

python manage.py diffsettings | grep -E 'STATIC_URL|STATIC_ROOT'
DJANGO_SETTINGS_MODULE=project.settings.production python manage.py diffsettings | grep -E 'STATIC_URL|STATIC_ROOT'

Rollback note: before changing settings paths, keep a copy of the current config and do not delete any existing working static directory until the new one is verified.

3) Run and verify collectstatic

Run collectstatic with the same settings your production app uses.

python manage.py collectstatic --noinput

If you use a custom settings module:

DJANGO_SETTINGS_MODULE=project.settings.production python manage.py collectstatic --noinput

Then verify files exist in STATIC_ROOT:

ls -lah /srv/app/staticfiles
ls -lah /srv/app/staticfiles/admin/css

You should see collected files, including admin assets if django.contrib.admin is installed.

If collectstatic fails, read the error carefully. Common causes:

  • bad permissions on STATIC_ROOT
  • missing frontend build output
  • manifest mismatch when using hashed static storage

Verification check:

stat /srv/app/staticfiles
python manage.py findstatic admin/css/base.css

If collectstatic completed but the files are missing from disk, double-check STATIC_ROOT and the runtime settings module.

Rollback note: if your deployment uses release directories or symlinks, keep the previous collected static directory available until the new release has passed an asset check. Do not switch traffic to a release that references assets not yet collected.

4) Fix reverse proxy static file serving

If Nginx or Caddy serves static files, the request path and server mapping must resolve to the correct filesystem path.

Nginx example

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/app/staticfiles/;
        access_log off;
        expires 1d;
        add_header Cache-Control "public";
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

For troubleshooting, the important point is:

  • location /static/ must match STATIC_URL = "/static/"
  • alias /srv/app/staticfiles/; must point to the collected files directory

Test config before reload:

sudo nginx -t
sudo systemctl reload nginx

Then test a known asset directly:

curl -I https://example.com/static/admin/css/base.css

Caddy example

example.com {
    handle /static/* {
        root * /srv/app/staticfiles
        file_server
    }

    reverse_proxy 127.0.0.1:8000
}

Validate and reload:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

If static files still fail, compare the request URL with the real file path on disk.

5) Fix app-served static files with WhiteNoise

WhiteNoise is appropriate for smaller deployments, single-container setups, or environments where you do not want a separate static file service layer.

Example configuration:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ...
]

STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

Then run:

python manage.py collectstatic --noinput

Common WhiteNoise failure cases:

  • middleware missing
  • middleware in the wrong place
  • frontend assets were not built before collectstatic
  • manifest storage references files that do not exist

Verification check:

curl -I https://example.com/static/admin/css/base.css

If Gunicorn is serving the app and WhiteNoise is configured correctly, static requests should return 200 without relying on Nginx file mapping.

After changing middleware or storage settings, reload or restart the application process before retesting.

Critical deploy note: if you use manifest-based static storage, deploy the application code and collected static files together. A partial deploy or rollback that changes templates or asset references without updating static files can break CSS or JS loading.

6) Check file permissions and ownership

Even with correct paths, the serving process must be able to read the files.

Inspect directory traversal permissions:

namei -l /srv/app/staticfiles
stat /srv/app/staticfiles

Things to verify:

  • each parent directory is executable (x) so it can be traversed
  • files are readable by the process user or group
  • mounted volumes in Docker are present and readable

If using Nginx, make sure the Nginx worker user can read the directory. If using WhiteNoise, the app process user must be able to read it.

SELinux can also block access on some systems, but treat that as an edge case after path and permission checks.

7) Check Docker and CI/CD-specific causes

In containerized deployments, static file problems often come from build ordering or runtime mounts.

Example Dockerfile pattern aligned with STATIC_ROOT = "/srv/app/staticfiles":

FROM python:3.12-slim

WORKDIR /srv/app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV DJANGO_SETTINGS_MODULE=project.settings.production

RUN python manage.py collectstatic --noinput

CMD ["gunicorn", "project.wsgi:application", "--bind", "0.0.0.0:8000"]

Common failures:

  • collectstatic ran before app files were copied
  • multi-stage build omitted the generated static directory
  • container starts with an empty mounted volume replacing image contents
  • release references hashed asset names not present in the mounted static volume

Verification checks:

docker exec -it <container> ls -lah /srv/app/staticfiles
docker logs <container>

If you use a shared volume for static files, confirm the current release and the collected files match.

Rollback note: when rolling back a container image, make sure the static volume or mounted directory also matches that release. Code from one release and manifest-hashed assets from another can break pages even if both exist.

8) Verify the fix end to end

After any change, test the full path.

Check a known asset directly:

curl -I https://example.com/static/admin/css/base.css

Then inspect logs:

journalctl -u nginx --since "10 minutes ago" || sudo tail -n 200 /var/log/nginx/error.log
docker logs <container>

Look for:

  • 404 on /static/...
  • permission denied errors
  • startup errors from collectstatic
  • wrong content type for CSS or JS

Also rule out cache effects:

  • hard refresh browser
  • test with curl
  • purge CDN cache if you use one

Do not remove the previous static directory or previous release artifact until the new asset URL checks return 200.

Explanation

This works because Django’s production static workflow has two separate parts:

  1. collection: collectstatic gathers files into STATIC_ROOT
  2. serving: Nginx, Caddy, or WhiteNoise serves those files at STATIC_URL

Most failures happen because one side is correct and the other is not. For example:

  • collectstatic succeeded, but Nginx points to the wrong directory
  • Nginx is correct, but STATIC_ROOT is empty
  • WhiteNoise is enabled, but assets were never collected
  • Docker image has static files, but a runtime volume hides them

Choose one clear strategy:

  • Nginx/Caddy serves static files for common VM or VPS deployments
  • WhiteNoise serves static files for simpler app-hosted deployments

Using Nginx for /static/ while WhiteNoise is also installed is not automatically broken, but it often makes troubleshooting less clear. Pick one serving path you actually rely on and verify it end to end.

When to automate this

If every deploy requires manual collectstatic, proxy validation, and asset smoke testing, this is a good place to introduce a reusable script or deployment template. The first parts worth automating are collectstatic, config validation, and a post-deploy check against one known static asset URL. That reduces avoidable release mistakes without changing your application architecture.

Edge cases / notes

  • Hashed frontend assets: if your frontend build produces hashed filenames, build assets before collectstatic. Otherwise manifest-based storage may reference missing files.
  • Static vs media: do not use this guide to fix user-uploaded files. That is a separate storage path and serving setup. See Django static files vs media files in production.
  • Development vs production: the Django development server serves static files automatically. Production does not.
  • CDN or object storage: valid production architectures, but outside the main troubleshooting path here.
  • Proxy/TLS headers: usually unrelated to static 404s, but incorrect proxy configuration can still affect app behavior elsewhere.
  • Migrations: database migrations do not fix static file issues, but make sure your release process handles both separately.

For related deployment patterns, see:

FAQ

Why do Django static files load locally but not in production?

Because the development server serves static files automatically, while production requires an explicit strategy. You must collect files into STATIC_ROOT and serve them through Nginx, Caddy, or WhiteNoise.

Do I need to run collectstatic on every deploy?

Usually yes, if static assets can change between releases. A safe deployment process runs collectstatic during build or release and verifies at least one known asset URL before marking the deploy healthy.

Should Nginx serve Django static files or should I use WhiteNoise?

Use Nginx or Caddy when you already have a reverse proxy and want a conventional production setup. Use WhiteNoise for simpler deployments, especially single-container apps, where keeping static serving inside the app process is acceptable.

Why is Django admin missing CSS in production?

That usually means one of three things: collectstatic was not run, /static/ is mapped to the wrong directory, or the serving process cannot read the collected files. Test /static/admin/css/base.css directly to narrow it down.

How do I verify that /static/ is mapped to the correct directory?

Check the URL being requested, then compare it to the real file path on disk. For example, if /static/admin/css/base.css is requested, the file should exist under your collected static directory, such as /srv/app/staticfiles/admin/css/base.css, and your proxy or WhiteNoise setup must serve that path successfully.

2026 · django-deployment.com - Django Deployment knowledge base