Last modified: Oct 14, 2026

Serve Static Files with WhiteNoise

Static files are easy in development. Django serves them automatically when DEBUG is on. Then you deploy, turn debug off, and everything breaks.

CSS files return 404. Images vanish. The site looks broken to every visitor.

WhiteNoise solves this problem. It lets your Django app serve its own static files in production, without nginx or a cloud storage bucket.

This guide explains how WhiteNoise serves those files, how to configure it properly, and how to avoid the errors that trip up most beginners.

What WhiteNoise Does

WhiteNoise is a middleware layer that sits inside your WSGI application. It intercepts requests for static files before they reach Django's view layer.

When a browser asks for a CSS file, WhiteNoise answers directly. It does not run URL routing, middleware chains, or database queries.

That makes static file delivery very fast. The request never touches your application logic.

WhiteNoise also handles the details that matter in production. It compresses files, sets long cache lifetimes, and adds content hashes to filenames.

Those are the same optimizations you would otherwise configure inside a web server. WhiteNoise just does them in Python.

Why Not Just Use nginx?

nginx is excellent at serving static files. If you already run nginx in front of your app, it is a fine choice.

But many deployments do not have that option. Platforms like Heroku and other PaaS providers run your app in a container with no separate web server.

Adding nginx means adding another process to manage, another configuration file to maintain, and another thing that can break.

WhiteNoise removes that layer. Your app becomes self-contained. One process serves both dynamic pages and static assets.

For small and medium sites, that simplicity is worth more than the marginal speed gain from nginx.

How the Request Flow Works

Understanding the order of operations makes configuration easier.

A browser sends a request for /static/css/style.css. The request arrives at your WSGI server.

WhiteNoise checks its internal file index. If the file exists in STATIC_ROOT, it returns the contents immediately.

If the file does not exist, WhiteNoise passes the request to the next middleware. Django then handles it as a normal URL request.

That is why middleware placement matters. The earlier WhiteNoise runs, the fewer layers a static request has to pass through.

If you have not installed the package yet, this guide on how to install whitenoise in Django covers the pip command and the middleware entry in detail.

Where Static Files Come From

Django collects static files from several places. Each installed app can ship its own static directory.

You can also define extra source directories with the STATICFILES_DIRS setting.

 # settings.py import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") STATICFILES_DIRS = [ os.path.join(BASE_DIR, "assets"), ] 

The STATIC_URL setting is the prefix used in generated URLs. The STATIC_ROOT setting is the destination folder for collected files.

The STATICFILES_DIRS list holds project-level source folders. Do not put STATIC_ROOT inside that list. Django raises an error if you do.

Collecting Files into One Place

WhiteNoise reads from a single directory. Django's collectstatic command builds that directory by copying files from every source.

 python manage.py collectstatic --noinput 

The --noinput flag skips the confirmation prompt. That matters in automated deployments where no human is present to type "yes".

The output shows how many files were copied.

 Copied 312 files to '/srv/app/staticfiles' 

Run this command on every deploy. Static files change whenever you edit CSS or upgrade a package that ships its own assets.

Enabling Compression and Hashing

The default WhiteNoise setup serves files as-is. You can do better by switching the static files storage backend.

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

The CompressedManifestStaticFilesStorage backend does two things at once. It creates gzip and Brotli versions of each file, and it appends a content hash to each filename.

So style.css becomes style.4f8a1c2d.css. When the file changes, the hash changes, and the browser fetches the new version.

Django rewrites the {% static %} template tag to point at the hashed name. That is why you should always use the tag instead of hardcoding paths.

 {% load static %} 

The rendered HTML contains the hashed filename. Users never see a stale stylesheet again.

How Caching Headers Work

WhiteNoise sets cache headers based on the filename. Hashed files get an aggressive policy. Everything else gets a short one.

You can inspect the headers with a simple curl request against a running server.

 curl -I http://localhost:8000/static/css/style.4f8a1c2d.css 

The response includes the caching directives.

 HTTP/1.1 200 OK Content-Type: text/css Cache-Control: max-age=315360000, public, immutable Content-Encoding: gzip 

The immutable directive tells the browser never to revalidate the file. Because the filename contains a hash, that is completely safe.

The Content-Encoding header shows the file was served compressed. The browser decompresses it transparently.

Customizing Which Files Are Immutable

By default, WhiteNoise treats any file with a hash-like name as immutable. You can change that rule.

Pass a callable to the constructor and WhiteNoise uses it to decide.

 # settings.py from whitenoise import WhiteNoise def immutable_file_test(path, url): """Treat files in /static/v1/ as immutable.""" return url.startswith("/static/v1/") WHITENOISE_IMMUTABLE_FILE_TEST = immutable_file_test 

The WHITENOISE_IMMUTABLE_FILE_TEST setting accepts a dotted path or a callable. The callable receives the file path and the URL.

This is useful when you serve versioned assets from a CDN or a fixed directory structure.

For frameworks that bundle their own asset pipeline, the rule may need adjustment. Projects like Wagtail ship their own static files with predictable naming, so a custom test can help when you install Wagtail CMS with Django in Python and want consistent cache behavior across both systems.

Adding Extra Directories at Runtime

Sometimes you need to serve files that are not part of Django's static pipeline. Reports, uploaded exports, or generated files fit that description.

WhiteNoise lets you register extra directories at startup.

 # wsgi.py import os from django.core.wsgi import get_wsgi_application from whitenoise import WhiteNoise os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = get_wsgi_application() application = WhiteNoise(application, root="/var/www/reports") application.add_files("/var/www/exports", prefix="exports/") 

The WhiteNoise() constructor wraps the Django application. The add_files() method registers additional directories after the initial setup.

Files under exports/ are now reachable at /exports/filename. The prefix keeps them separate from your regular static files.

Development Versus Production

WhiteNoise works in development too, but you do not need it there. Django's built-in static file server handles that case.

The important test is running with DEBUG = False on your local machine. That is the only way to confirm production behavior before deploying.

 python manage.py collectstatic --noinput python manage.py runserver --insecure 

The --insecure flag forces the development server to serve static files even with debug off. It is for testing only. Never use it in production.

Open your site and check that every asset loads. Look at the browser network tab for any 404 responses.

If a file is missing, Django's static file finder can tell you where it searched.

 python manage.py findstatic css/style.css --verbosity 2 

The output lists every directory Django checked. That usually reveals a wrong path or a missing file immediately.

Common Errors and Fixes

Static files return 404 with debug off. The middleware is missing, or collectstatic never ran. Check both before anything else.

"Missing staticfiles manifest entry" appears on a page. A template references a file that does not exist in the manifest. Run the findstatic command to locate the gap.

"The file could not be found" during collectstatic. A CSS file references an image or font that is missing. Post-processing tries to rewrite those URLs and fails when the target is absent.

Tests run slowly. WhiteNoise scans all static files when the app starts. Frequent restarts during tests multiply that cost. Set WHITENOISE_AUTOREFRESH = True in your test settings to skip the scan.

Brotli files are never created. The Brotli codec is an optional dependency. Install the package with the extra to enable it.

Startup is slow on a large project. A huge node_modules folder inside your static sources forces WhiteNoise to index thousands of files. Keep it out of the collected directories.

Best Practices

Use the manifest storage backend. Content hashing enables long cache lifetimes without stale file risk.

Always use the static template tag. Hardcoded paths break the moment you switch storage backends.

Run collectstatic on every deploy. Add it to your build step so nobody has to remember.

Test with debug off locally. A two-minute check saves a production incident.

Pin the package version. Storage behavior can change between releases. A pinned version keeps deployments predictable.

Consider a CDN for heavy traffic. WhiteNoise sets correct cache headers, so a CDN can absorb most static requests without touching your app.

Deploying with Static Files Handled

Once WhiteNoise is configured, deployment becomes simpler. There is no extra web server to configure and no bucket to provision.

The deploy process is the same as any Django app, with one extra step for collectstatic. Automating that step avoids a whole class of production bugs.

If you want that automation handled for you, this guide on how to deploy a Django app with Fabric shows how to script the release so static files are collected on every push.

Conclusion

WhiteNoise lets your Django app serve its own static files in production. No nginx, no S3 bucket, no extra process to manage.

The setup takes four steps. Install the package, add the middleware, configure the storage backend, and run collectstatic.

From there, WhiteNoise handles compression, cache headers, and content hashing automatically. Those are the same optimizations a web server would apply.

Test with debug off before you deploy. Check the response headers. Confirm every asset loads.

Do that once, and static files stop being a production problem. Your app ships as a single, self-contained unit that works anywhere Python runs.