Deployment
#django
#whitenoise

How to Serve Django Static Files with WhiteNoise

Django does not safely serve static files in production by default. With DEBUG=False, you still need a production path for CSS, JavaScript, fonts, and images collected from your...

Problem statement

Django does not safely serve static files in production by default. With DEBUG=False, you still need a production path for CSS, JavaScript, fonts, and images collected from your app and dependencies. If that path is missing or misconfigured, the first visible symptom is usually broken styling, including unstyled Django admin pages.

WhiteNoise is a practical option for many Django deployments because it lets the Django application serve pre-collected static files directly. That removes the need for a separate static file mapping in Nginx for simpler setups, especially on small to medium deployments, containerized apps, and single-service environments.

It is not the right tool for everything. WhiteNoise is for versioned static assets that are part of your application release. It is not for user-uploaded media, private files, or very large-scale asset distribution where object storage and a CDN are better fits.

Quick answer

To serve Django static files with WhiteNoise in production:

  1. Install whitenoise
  2. Add WhiteNoiseMiddleware directly after SecurityMiddleware
  3. Set STATIC_URL and STATIC_ROOT
  4. Set ALLOWED_HOSTS for production
  5. Enable CompressedManifestStaticFilesStorage
  6. Run python manage.py collectstatic --noinput during build or release
  7. Deploy the new code and restart the app process
  8. Verify that /static/ assets load and include cache headers

Keep DEBUG=False, and do not route user-uploaded media through WhiteNoise.

Step-by-step solution

1. Install WhiteNoise

Install the package in the same environment used for production:

pip install whitenoise

Pin it in your dependency file:

whitenoise==6.9.0

If you deploy with Docker, make sure this dependency is installed during image build, not only in local development.

Verification

Check that the package is available:

python -c "import whitenoise; print(whitenoise.__version__)"

2. Configure Django settings for static files

Your production settings need a public URL path, a collection directory, and valid hostnames:

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

DEBUG = False
ALLOWED_HOSTS = ["example.com"]

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

If your project also has source static directories, keep them in STATICFILES_DIRS:

STATICFILES_DIRS = [
    BASE_DIR / "static",
]

STATIC_ROOT is where collectstatic writes the final deployment-ready files. It should not be the same directory as your source static files.

Verification

Run:

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

You should see at least one resolved path. If not, your static files configuration is incomplete before WhiteNoise is even involved.

3. Add WhiteNoise middleware

Add the middleware directly after Django’s security middleware:

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

Order matters. WhiteNoise should be near the top so it can handle static file requests early and efficiently.

Verification

Start the app with DEBUG=False and request a known static path after running collectstatic. If static files only work when DEBUG=True, the production setup is still wrong.

4. Enable hashed and compressed static file storage

For current Django versions, use the STORAGES setting:

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

For older Django versions, use:

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

This storage backend does two important things:

  • creates hashed filenames such as app.4f3c1d2a.css
  • generates compressed versions when appropriate

Hashed filenames make long-term caching safe because browsers fetch a new file when the content changes.

Verification

Run:

python manage.py collectstatic --noinput

Then inspect the output in STATIC_ROOT. You should see fingerprinted files and a manifest file.

5. Run collectstatic during deployment

WhiteNoise serves files that already exist in STATIC_ROOT. That means collectstatic must run as part of your deployment workflow.

Non-Docker release step:

python manage.py collectstatic --noinput

Example with systemd and Gunicorn release flow:

cd /srv/myapp/current
source /srv/myapp/venv/bin/activate
python manage.py migrate --noinput
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn

Dockerfile example:

# Only if your build has access to the project code, settings, and required env
RUN python manage.py collectstatic --noinput

In Docker deployments, running collectstatic at image build time is usually more predictable than doing it at container startup, provided the build environment can import Django settings safely. Otherwise, use a dedicated release step.

Verification

After collectstatic, confirm files exist:

ls -la staticfiles/

If the command fails, do not continue the deployment. A failed manifest build can break asset references in production.

Rollback note

If a release includes template or frontend changes, roll back code and static assets together. Do not keep old code with new static manifest files or vice versa.

6. Deploy and restart the app process

After static files are collected, deploy the updated release and restart the app service if your process manager requires it.

Example:

sudo systemctl restart gunicorn

Or perform a controlled container rollout through your deployment tool.

The important point is that the running application and the collected static manifest belong to the same release. In multi-instance deployments, avoid serving mixed old and new releases behind the load balancer longer than necessary.

Verification

Check that the app starts cleanly and that requests do not raise missing manifest entry errors in logs or error reporting.

7. Verify static files in production

Use a known asset first. Django admin CSS is a good target because it is present on most projects.

Check headers:

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

You should see a successful response and appropriate content type. With hashed assets, you should also see cache-friendly headers.

To test a fingerprinted file, use an actual generated filename from STATIC_ROOT or from page source. For example:

find staticfiles -type f | grep -E '\.[0-9a-f]{8,}\.(css|js)$' | head

Then test one real generated file:

curl -I https://example.com/static/path/to/generated-file.<hash>.css

What to verify:

  • the file returns 200 OK
  • Content-Type matches the asset type
  • cache headers are present
  • admin pages load with styling
  • application logs do not show repeated 404s for static assets

If you are behind Nginx or Caddy, verify the reverse proxy is forwarding requests correctly and not intercepting /static/ with a broken local path rule. WhiteNoise does not replace correct proxy, HTTPS, or upstream configuration.

Config/code examples

Minimal settings.py changes

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

DEBUG = False
ALLOWED_HOSTS = ["example.com"]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

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

Release workflow example

python manage.py migrate --noinput
python manage.py collectstatic --noinput
systemctl restart gunicorn

Validation commands

python manage.py collectstatic --noinput
python manage.py findstatic admin/css/base.css
curl -I https://example.com/static/admin/css/base.css

Explanation

How WhiteNoise works in production

WhiteNoise serves static assets from the collected output directory through your Django app process. Instead of configuring a separate web server path for static files, you let the app return those files directly. This is why collectstatic is required: WhiteNoise serves the built static output, not your source static directories.

Why manifest storage improves reliability

Manifest storage rewrites references to use hashed filenames. That prevents stale browser caches after a deployment. Without hashing, a browser may keep using an old app.css even though the new release expects different asset contents.

This also makes rollback safer when your release process is atomic. Each release carries a matching set of templates, code, and static asset names.

Security and performance notes

WhiteNoise is for public static assets only. Do not use it for user-uploaded media, private reports, or access-controlled files. Those need a different storage and delivery path.

Keep DEBUG=False in production and set ALLOWED_HOSTS correctly for the deployed domain names. If you run behind Nginx or Caddy, let the proxy handle HTTPS termination and upstream forwarding while WhiteNoise handles static asset responses inside the app. WhiteNoise is not a substitute for correct reverse proxy or HTTPS settings.

When to automate this setup

If you deploy more than one environment or release frequently, this manual flow should become a script or template. The repeatable parts are dependency pinning, middleware insertion, collectstatic, service restart, and post-deploy asset checks. Automating those steps reduces the chance of broken admin CSS or mismatched manifests during release.

Edge cases and notes

WhiteNoise with Docker

In Docker, prefer running collectstatic during image build only when the build can safely import Django settings and has the required environment. Otherwise, run it in a dedicated release job. Avoid doing it independently in every container on startup, especially with multiple replicas.

WhiteNoise behind Nginx or Caddy

You can still use WhiteNoise behind a reverse proxy. Nginx or Caddy handles TLS and proxies requests to Gunicorn or Uvicorn. WhiteNoise can simplify the static file side by removing the need for a separate /static/ filesystem mapping.

WhiteNoise with Django admin

If the Django admin is missing CSS, check these first:

  • collectstatic actually ran
  • STATIC_ROOT exists and contains files
  • WhiteNoiseMiddleware is in the correct position
  • manifest storage is configured correctly
  • ALLOWED_HOSTS is valid and the app is actually serving requests
  • the deployed release and manifest match

Handling rollback safely

Rollback should restore both application code and the matching static assets. If your templates reference app.4f3c1d2a.css but the rollback only restores code and not static files, users may get 404s for hashed asset paths. Immutable images or atomic release directories help avoid this mismatch.

What WhiteNoise does not handle

WhiteNoise does not handle:

  • user-uploaded media files
  • private file delivery
  • large-scale CDN or object-storage architecture by itself

If you need those, use a separate media storage design.

For the underlying concepts, see Django static files in production: collectstatic, STATIC_ROOT, and STATIC_URL explained.

If you are comparing deployment architectures, see Deploy Django with Gunicorn and Nginx on Ubuntu, Deploy Django ASGI with Uvicorn and Nginx, and Deploy Django with Caddy and Automatic HTTPS.

If static assets still fail after deployment, use why Django static files are not loading in production as a troubleshooting checklist, and review the broader Django deployment checklist for production.

FAQ

Can WhiteNoise replace Nginx for serving Django static files?

For many small and medium Django deployments, yes, WhiteNoise can serve static files without a separate Nginx static file mapping. But Nginx may still be useful for TLS termination, reverse proxying, buffering, and broader traffic handling.

Do I still need collectstatic when using WhiteNoise?

Yes. WhiteNoise serves the files produced by collectstatic. If you skip that step, your production app will not have the complete static asset set or manifest.

Why is the Django admin missing CSS after deployment?

Usually one of these is wrong:

  • collectstatic did not run
  • STATIC_ROOT is incorrect
  • WhiteNoiseMiddleware is missing or ordered incorrectly
  • manifest storage is misconfigured
  • ALLOWED_HOSTS or proxy routing prevents the app from serving requests correctly
  • the release restarted with code that does not match the current static manifest

Should I use WhiteNoise for user-uploaded media files?

No. WhiteNoise is for static assets that are part of your application release. User-uploaded media should use a separate storage and serving path.

Is WhiteNoise suitable for Docker deployments?

Yes. It works well in Docker if you run collectstatic during image build only when the build environment can import Django settings safely, or in a controlled release step that produces a matching image and static asset set.

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