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:
- Install
whitenoise - Add
WhiteNoiseMiddlewaredirectly afterSecurityMiddleware - Set
STATIC_URLandSTATIC_ROOT - Set
ALLOWED_HOSTSfor production - Enable
CompressedManifestStaticFilesStorage - Run
python manage.py collectstatic --noinputduring build or release - Deploy the new code and restart the app process
- 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-Typematches 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:
collectstaticactually ranSTATIC_ROOTexists and contains filesWhiteNoiseMiddlewareis in the correct position- manifest storage is configured correctly
ALLOWED_HOSTSis 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.
Internal links
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:
collectstaticdid not runSTATIC_ROOTis incorrectWhiteNoiseMiddlewareis missing or ordered incorrectly- manifest storage is misconfigured
ALLOWED_HOSTSor 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.