Serve Django Static Files on Kubernetes
Choose how to serve Django static files on Kubernetes: WhiteNoise inside the app image, an Nginx sidecar sharing an emptyDir, a separate static image, or object storage with a CDN, and avoid shared volumes, stale files, and collectstatic at pod start.
Problem statement
On a single server, Nginx serves STATIC_ROOT from disk. On Kubernetes, pods are replaced constantly, there is no shared disk by default, and several replicas run different pods. Teams often try a ReadWriteMany volume shared between pods and Nginx, or run collectstatic at container start, and end up with slow startups, stale files, or 404s during rollouts. Better options keep static files inside the release that references them.
- Run
collectstaticin the Dockerfile, not at pod start. - Default choice: serve static files from Django with WhiteNoise and put a CDN in front for heavy traffic.
- If you need Nginx, add it as a sidecar that copies files from the app image via an
emptyDirin an init container. - For large asset sets or many services, upload to object storage during CI and serve through a CDN.
- Avoid ReadWriteMany volumes for static files; they break rollouts and add a storage dependency.
Step-by-step solution
1. Collect at build time
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput
A small build settings module avoids needing secrets or a database during the build. The image now contains the exact static files for its code.
2. Serve with WhiteNoise
MIDDLEWARE = ["django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", ...]
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
WhiteNoise serves hashed files with far-future cache headers and precompressed Brotli and gzip. During a rolling update, old pods serve old assets and new pods serve new ones, so HTML and assets always match. A CDN in front caches files after the first request, keeping load off Gunicorn.
3. Nginx sidecar, when required
Some teams need Nginx features (custom headers, X-Accel-Redirect for private media). Copy static files from the app image into a shared emptyDir with an init container:
spec:
volumes:
- name: static
emptyDir: {}
initContainers:
- name: copy-static
image: ghcr.io/acme/shop:3f2a1c9
command: ["sh", "-c", "cp -r /app/staticfiles/. /static/"]
volumeMounts: [{ name: static, mountPath: /static }]
containers:
- name: web
image: ghcr.io/acme/shop:3f2a1c9
command: ["gunicorn", "config.wsgi", "--bind", "127.0.0.1:8000"]
- name: nginx
image: nginx:1.27-alpine
ports: [{ containerPort: 8080 }]
volumeMounts:
- { name: static, mountPath: /srv/static, readOnly: true }
- { name: nginx-conf, mountPath: /etc/nginx/conf.d }
Each pod has its own copy, matching its own code.
4. Object storage and CDN
For many services or very large asset sets, upload static files during CI with collectstatic configured for an S3 storage backend, keeping hashed names so releases never overwrite each other's files. Set STATIC_URL to the CDN domain. Old files remain available for pods still running the previous release; clean them up later with a lifecycle rule.
5. Avoid the shared volume trap
A ReadWriteMany volume mounted by every pod and filled by collectstatic at startup means a new pod overwrites the manifest that old pods still use, and pages reference files that changed underneath them. It also adds a storage system (NFS, EFS) to your critical path. Keep media (user uploads) in object storage and static files in the image.
Explanation
Django's manifest storage maps logical file names to hashed names, and the manifest itself is a static file. Serving HTML from one release and assets from another breaks whenever names change. Kubernetes rollouts run two releases side by side, so static files must travel with the code: inside the image (WhiteNoise or sidecar) or in an append-only, versioned bucket. Building at image time also makes pod startup fast and deterministic.
Edge cases / notes
- Admin assets. Django admin files are collected like any others; they work with all three options.
- Ingress caching. An ingress or Gateway with caching can reduce load for WhiteNoise without a CDN.
- Media uploads. User uploads never belong in the image; use object storage with django-storages.
Next steps
- Configure the default option with Serve Static Files With WhiteNoise.
- Compare servers in WhiteNoise vs Nginx for Static Files.
- Avoid volume pitfalls described in Fix Django Static Files Not Updating With a Docker Volume.
FAQ
Should collectstatic run when the pod starts?
No. Run it during the image build so pods start quickly and each release has its own files.
Is WhiteNoise fast enough on Kubernetes?
Yes, especially with a CDN in front. It serves compressed files with long cache headers.
Can several pods share one static volume?
They can, but it breaks rolling updates and adds a storage dependency. Keep static files in the image.
Where should user uploads go?
To object storage such as S3, GCS, or R2, not to the image or a pod volume.