Fix Django Static Files Not Updating With a Docker Volume

Fix Django CSS and JavaScript that stay old after a Docker deploy because a named volume hides the new collectstatic output: understand how volumes shadow image files, collect at the right time, and share static files with Nginx correctly.

Problem statement

You changed a stylesheet, rebuilt the image, and redeployed. The new HTML references the new file names, but the browser gets 404s for them, or the old CSS keeps showing. Inside the container, /app/staticfiles has last month's files. The Compose file mounts a named volume there so Nginx can serve it:

services:
  web:
    volumes:
      - static:/app/staticfiles
  nginx:
    volumes:
      - static:/srv/static:ro
volumes:
  static:

Docker copies an image's files into a named volume only the first time the volume is created and empty. On every later deploy, the existing volume is mounted over the image's directory and hides the new collectstatic output you baked into the image.

Why files stay oldNew image builtcollectstatic insideVolume existsfrom first deployMounted over dirhides new filesNginx serves old404 or stale CSSA named volume is only seeded once; after that it shadows the image directory.
  1. Confirm: docker compose exec web ls -la /app/staticfiles | head shows old timestamps.
  2. Quick fix: remove the volume and redeploy: docker compose down && docker volume rm myapp_static && docker compose up -d.
  3. Lasting fix A: run collectstatic when the container starts (into the volume), not only during the build.
  4. Lasting fix B: drop the volume and serve static files from the app with WhiteNoise.
  5. Lasting fix C: build a separate Nginx image that copies static files from the app image.

Step-by-step solution

1. Confirm the volume is the cause

docker compose exec web python -c "import django,os; print(sorted(os.listdir('/app/staticfiles'))[:5])"
docker volume inspect myapp_static --format '{{.CreatedAt}}'
docker run --rm --entrypoint ls myapp-web:latest -la /app/staticfiles | head

If the image has new files but the running container shows old ones, and the volume was created long ago, the volume is shadowing the image.

2. Collect into the volume at startup

Run collectstatic in the entrypoint or a one-off release step, after the volume is mounted:

#!/bin/sh
set -e
python manage.py collectstatic --noinput
exec "$@"

collectstatic copies new and changed files into the volume. With ManifestStaticFilesStorage, old hashed files remain, which is useful: browsers with cached old pages still find their assets during the rollout. Clean up occasionally with --clear during a maintenance window if size matters.

deploys over timeFirst deployvolume seededSecond deploynew image filesVolume mountedover the directoryOld files servednew names 404collectstatic at startvolume updatedOnly the first deploy copies image files into the volumedjango-deployment.com
Anything baked into an image path is invisible once a volume is mounted there.

3. Or remove the volume entirely

WhiteNoise serves static files from the Django process with long cache headers and compression. No shared volume, no Nginx static block:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ...
]
STORAGES = {
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

Files collected during the build stay in the image and are always current. Put a CDN in front if traffic is high. See Serve Static Files With WhiteNoise.

4. Or bake static files into the Nginx image

If Nginx must serve static files, build its image from the app image so both always match:

# nginx/Dockerfile
FROM myapp-web:${RELEASE} AS app
FROM nginx:1.27-alpine
COPY --from=app /app/staticfiles /srv/static
COPY nginx.conf /etc/nginx/conf.d/default.conf

Each release produces a matching pair of images, and rollbacks restore both.

5. Check browser and CDN caches

If files on disk are correct, the stale content is cached elsewhere. Hashed file names from ManifestStaticFilesStorage make cache invalidation unnecessary. Without them, users keep old CSS until their cache expires. Purge the CDN or enable hashed names.

where should static files live?What serves static files today?Django container onlyWhiteNoiseno volume neededNginx with shared volumecollect at startvolume refreshedNginx, strict releasesNginx imagecopies from appAvoid relying on Docker seeding a volume from the imagedjango-deployment.com
Each option keeps static files tied to the release that references them.

Explanation

Docker's named volumes have one convenience feature: when an empty volume is first mounted at a path that has content in the image, Docker copies that content in. After that, the volume is independent of any image. Mounting it hides whatever the image contains at that path. That is great for databases and uploads, where you want data to survive image changes, and wrong for build artifacts like static files, which should change with every release.

Edge cases / notes

  • Bind mounts. A host directory bind-mounted over /app/staticfiles is never seeded at all; the directory starts empty and stays whatever is on the host.
  • Multiple web replicas. Several containers running collectstatic into one volume at once usually works, because they write identical files, but a release step that runs once is cleaner.
  • Media is different. Uploaded media must live in a volume or object storage precisely because it should survive deploys.

Next steps

FAQ

Why did it work on the first deploy?

The volume was new and empty, so Docker copied the image's files into it. Later deploys reuse the volume without copying.

Is deleting the volume safe?

For static files, yes; they can be regenerated with collectstatic. Never delete volumes holding media uploads or databases.

Should collectstatic run at build time or start time?

At build time for images that serve their own files (WhiteNoise). At start time if a shared volume must be refreshed.

Does --clear help?

It deletes old files before collecting. Useful occasionally, but it can break pages that still reference old hashed names during a rollout.

Keep going