Jake Groszewski

Blog

Django Static Files in 2026: How to Serve Assets with WhiteNoise the Right Way

Static files trip up more Django deployments than almost anything else. You get the app running, hit the URL, and your CSS is just gone. Or you deploy to a VPS, run collectstatic, and somehow the browser is still pulling stale assets from three releases ago. These are not exotic edge cases. They're the ordinary friction that comes with Django's default static file setup, which works fine in development and requires deliberate configuration for production.

WhiteNoise is the standard answer for teams that don't want to stand up a separate NGINX config or deal with a CDN origin just to serve a handful of CSS and JavaScript files. The WhiteNoise documentation presents it explicitly as a deployment-oriented middleware solution, and the setup is straightforward once you understand what each piece does.

This tutorial walks through the full configuration: middleware ordering, STATICFILES_DIRS, collectstatic, compression, caching headers, and the mistakes most people make along the way.

What WhiteNoise Actually Does

Out of the box, Django's built-in staticfiles app can serve files during development via runserver, but it explicitly warns against using that in production. In a typical production setup without WhiteNoise, you'd configure NGINX or Apache to serve the static directory directly, bypassing Django entirely. WhiteNoise sits in Django's middleware stack and does something similar at the Python layer: it intercepts requests for static files early, serves them directly from the file system, and adds proper HTTP headers without those requests ever reaching your view layer.

For small-to-medium Django apps, especially those deployed on a single VPS or a platform like Fly.io or Railway, this eliminates a layer of infrastructure without meaningful performance cost.

Installation

Start by installing the package:

pip install whitenoise

If you want Brotli and gzip compression (recommended), install with the extras:

pip install "whitenoise[brotli]"

Add it to your requirements.txt or pyproject.toml as appropriate.

Middleware Configuration

This is the part that most deployment guides skip over or get wrong. WhiteNoise must be placed directly below SecurityMiddleware in your MIDDLEWARE list. The WhiteNoise docs are explicit about this ordering, and it matters because SecurityMiddleware handles HTTPS redirects and security headers that should apply to everything, including static asset responses.

# settings.py

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",  # <-- directly below SecurityMiddleware
    "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",
]

Putting WhiteNoise after SessionMiddleware or later means session processing runs before static file requests are short-circuited, which is wasted overhead.

Static File Settings

You need three settings configured correctly:

# settings.py

import os
from pathlib import Path

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

# Where Django looks for static files in your project
STATICFILES_DIRS = [
    BASE_DIR / "static",
]

# Where collectstatic will copy all files to
STATIC_ROOT = BASE_DIR / "staticfiles"

# The URL prefix for static files
STATIC_URL = "/static/"

# Enable WhiteNoise compression and caching
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

The CompressedManifestStaticFilesStorage backend does two useful things. It generates hashed filenames (so main.css becomes main.abc123.css), which allows you to set aggressive cache headers without worrying that browsers will serve stale files after a deploy. And it compresses those files into .gz and .br variants at collection time so they're ready to serve without per-request compression.

Running collectstatic

Before deploying, you run collectstatic to gather all static assets from your app directories and STATICFILES_DIRS into STATIC_ROOT:

python manage.py collectstatic --no-input

In CI/CD pipelines, --no-input avoids the confirmation prompt. The output will tell you how many files were copied and how many were post-processed with hashes.

A common mistake here: forgetting to run collectstatic before starting the app in production. WhiteNoise serves from STATIC_ROOT, not from your source directories, so if you skip this step your static files just won't be there.

Caching Headers

WhiteNoise sets a one-year cache lifetime on hashed files by default, which is the correct behavior when you're using CompressedManifestStaticFilesStorage. Because the file hash changes whenever the content changes, a one-year cache on main.abc123.css is safe. Browsers that have that file cached will get a new URL (main.def456.css) on the next deploy.

For files that aren't hashed (which shouldn't exist in a well-configured setup, but sometimes do), WhiteNoise sets a short cache time. You can adjust the default max-age for autorefresh if needed:

# settings.py

# Default is 60 seconds for non-hashed files
WHITENOISE_AUTOREFRESH = False  # set True only during development
WHITENOISE_MAX_AGE = 31536000  # one year, in seconds

Don't set WHITENOISE_AUTOREFRESH = True in production. It disables file caching entirely and makes WhiteNoise check the filesystem on every request, which defeats the purpose.

Development vs. Production Settings

If you're using split settings (a common pattern), you typically want WhiteNoise only in production. In development, Django's built-in static file serving works fine and is easier to reason about.

# settings/production.py

from .base import *

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... rest of middleware
]
# settings/development.py

from .base import *

STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"

# MIDDLEWARE here does not include WhiteNoise

This avoids hash-based filenames during local development, where you want changes to show up immediately without re-running collectstatic.

Common Pitfalls

STATIC_ROOT not set. If STATIC_ROOT is undefined, collectstatic will fail with a configuration error. This is the most common source of confusion for developers who've only run Django locally.

Files not showing up after deploy. Usually means collectstatic didn't run, or it ran before the latest code was pulled. In a Dockerfile, run collectstatic as part of the build, not the entrypoint.

# Dockerfile excerpt
RUN python manage.py collectstatic --no-input

DEBUG = True in production. When DEBUG is True, Django handles static files itself via runserver behavior, and WhiteNoise is effectively bypassed for the purpose of serving the static directory. Always set DEBUG = False in production.

Missing staticfiles in INSTALLED_APPS. WhiteNoise depends on django.contrib.staticfiles being installed. It almost certainly is, but if you've stripped down a minimal Django config, double-check.

Using ManifestStaticFilesStorage without compression. If you don't want Brotli/gzip, use whitenoise.storage.CompressedStaticFilesStorage instead of the manifest version. The manifest version is generally preferable because it enables long-lived caching.

A Note on When to Use a CDN Instead

WhiteNoise is a good fit when you're running a single-server or small multi-server Django deployment and you don't want the overhead of configuring a CDN origin. For high-traffic apps serving large media files, or when you're already using something like Cloudflare in front of your app, you'll probably want to offload static files to S3 or a dedicated CDN bucket.

WhiteNoise's own documentation acknowledges this tradeoff. For most Django apps that don't need CDN-level scale, though, it's a pragmatic solution that keeps your infrastructure simple.

Verifying It Works

After deploying, you can check that WhiteNoise is serving files correctly by inspecting the response headers on a static asset:

curl -I https://yourapp.com/static/css/main.abc123.css

You should see:

HTTP/2 200
cache-control: max-age=31536000, public, immutable
content-encoding: br
content-type: text/css; charset=utf-8

The immutable directive in the cache header and br encoding (Brotli) confirm that WhiteNoise is active and compression is working. If you see content-encoding: gzip instead of br, the requesting client doesn't support Brotli, which is fine.

Actionable Takeaways

  • Place WhiteNoiseMiddleware directly below SecurityMiddleware, not lower in the stack.
  • Use CompressedManifestStaticFilesStorage for production to get hashed filenames and automatic compression.
  • Run collectstatic during your build step, not at runtime.
  • Set DEBUG = False in production, or WhiteNoise won't behave as expected.
  • Verify deployment with curl -I to confirm caching and compression headers are present.

For the full configuration reference, the WhiteNoise documentation covers additional options including serving files from multiple directories and integration with third-party storage backends.