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.

Static files on K8scollectstaticat image buildOption AWhiteNoise in appOption Bsidecar or CDNNo shared volumeper-release filesBuild static files into the image so each release carries its own assets.
  1. Run collectstatic in the Dockerfile, not at pod start.
  2. Default choice: serve static files from Django with WhiteNoise and put a CDN in front for heavy traffic.
  3. If you need Nginx, add it as a sidecar that copies files from the app image via an emptyDir in an init container.
  4. For large asset sets or many services, upload to object storage during CI and serve through a CDN.
  5. 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.

option and trade-offsRollout-safeComplexityWhiteNoise in appyeslowestNginx sidecaryesmediumObject storage + CDNyes, if versionedCI upload stepShared RWX volume!no!highKeep static files in the release; avoid shared volumesdjango-deployment.com
Hashed file names let old and new releases coexist during rollouts.

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.

which option fits?What do you need?simple, one serviceWhiteNoise + CDNno extra partsNginx features neededNginx sidecaremptyDir copymany services, big assetsBucket + CDNupload in CIAll three keep each release’s files togetherdjango-deployment.com
Running collectstatic at pod start is not one of the options.

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

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.

Keep going