skip to content

In a containerised Django deployment, why does collectstatic run when the image is built while migrate runs once per release before new code takes traffic?

level: middleimportance: must knowfreq 62%

answer

  1. what each command writes to
  2. files in the image vs shared database
  3. STATIC_ROOT and the hashed manifest
  4. schema must lead the code

basics

~20 s

collectstatic reads files already in the code and writes them to STATIC_ROOT, so its output belongs in the immutable image. migrate alters the shared database, so it runs once per release, before new-code containers serve requests.

solid answer

~40 s

`collectstatic` gathers every app's `static/` directory and `STATICFILES_DIRS` into `STATIC_ROOT`; when `STORAGES["staticfiles"]` uses `ManifestStaticFilesStorage` it also writes hashed file names and `staticfiles.json`. It needs no database and depends only on what is in the image, so running it once in the image build gives every container the same assets and the same manifest. `migrate` is the opposite: its target is the one database that old and new containers share, so it runs once per release as its own step rather than in every container's start command (Django's `migrate` takes no lock of its own), and it must finish before code that expects the new schema takes traffic, or queries naming new columns fail. The order is: build the image with `collectstatic`, run `migrate` once, then start the new containers.

code

bash · 7 lines
bash
# inside the image build: output depends only on the code
python manage.py collectstatic --noinput

# once per release, from the new image, against the production database
python manage.py migrate --noinput

# only after migrate succeeds: start or switch to the new app-server containers

go deeper

for a junior

Know that collectstatic copies static files into STATIC_ROOT and migrate applies schema changes to the database, and that production runs both as deliberate steps, not by hand.

for a middle

Explain why collectstatic belongs in the image build and migrate in a single release step, and what ManifestStaticFilesStorage writes and reads.

for a senior

Lay out the release order, name the failures when a step is misplaced, and explain why migrate must not run in every container's start command.

for a principal

Frame the build-versus-release split as a policy: immutable images, one schema change per release, and a pipeline that fails loudly at the cheapest point.

## Two commands, two very different targets A Django release usually involves two management commands that sit next to each other on a deploy checklist but touch completely different things. **`collectstatic`** (from `django.contrib.staticfiles`) reads files that ship with the code and writes them to a directory. **`migrate`** reads the migration files that ship with the code and writes to the **database**, the one piece of state that every running container, old and new, shares. | Command | Reads | Writes | Needs the database? | Natural home | |---|---|---|---|---| | `collectstatic` | each app's `static/` directory and `STATICFILES_DIRS` | `STATIC_ROOT`, plus `staticfiles.json` with `ManifestStaticFilesStorage` | No | the image build | | `migrate` | each app's `migrations/` package and the `django_migrations` table | the schema and the `django_migrations` table | Yes | once per release, before traffic | Where a step runs follows from what it writes. ## collectstatic belongs to the build `collectstatic` copies every static file its finders locate into `STATIC_ROOT`. When `STORAGES["staticfiles"]` points at `ManifestStaticFilesStorage`, it then **post-processes** the files: it renames each one with a content hash (`site.css` becomes something like `site.3f2a9c1b.css`), rewrites the references inside CSS, and writes a manifest, `staticfiles.json`, that maps original names to hashed names. - The output depends **only on the code and settings** in the image, so building it once guarantees that every container started from that image serves identical assets and reads an identical manifest. - It needs **no database connection** and runs only the system checks tagged `staticfiles`, so it fits a build step that has no production access. - Running it at container start instead repeats identical work on every boot, slows scaling, and needs a writable `STATIC_ROOT` at runtime. - A broken reference, such as a stylesheet pointing at an image that does not exist, fails post-processing and therefore **fails the build**, which is the cheapest place to find it. ## migrate belongs to the release `migrate` compares the migration graph in the code with the rows in `django_migrations` and applies whatever is missing. Its target is shared, so the reasoning flips: - It must run **once per release**, not once per container. Django's `migrate` takes no lock of its own, so several containers each running it on start can race to apply the same plan. - It must **finish before code that expects the new schema takes traffic**. The new code's queries name columns and tables that only exist after the migration; until then they fail with database errors. - It needs **production database credentials**, which a build step should not hold. An image built with them, or one whose build migrated a particular database, stops being environment-independent. Whether that single run is a separate release job, a pre-deploy hook or an init task is a platform choice. The Django-specific facts are the two above: one run, and before the new code serves. ## The release order 1. **Build** the image from the commit, running `collectstatic --noinput` inside the build so the hashed files and the manifest are part of the image. 2. **Run `migrate --noinput` once** against the production database, using the new image so the migration files match the code about to ship. 3. **Start the new containers** and let the platform's health checks decide when they receive traffic. 4. **Retire the old containers**, which for a short time run against the already-migrated schema. Step 4 is why migrations for a rolling release are written so the previous release keeps working against the new schema; that discipline is a subject of its own. ## What goes wrong when a step is misplaced - **`migrate` in the image build**: the build either cannot reach the database or migrates it before the release is approved, and the image is now tied to one environment. - **`migrate` in every web container's start command**: concurrent attempts on the same plan, and a failing migration shows up as containers that crash on boot rather than as one failed release step. - **New code before `migrate`**: requests that touch new fields fail until the migration lands. - **No `collectstatic` in the image**: with `DEBUG = False` and `ManifestStaticFilesStorage`, the `{% static %}` tag raises `ValueError: Missing staticfiles manifest entry` because `manifest_strict` defaults to `True`, so pages that reference static files return a 500. With `DEBUG = True` the storage returns unhashed URLs, which is why this bug hides in local development. - **`collectstatic --no-post-process` to get a failing build through**: no hashed files and no manifest are written, so the same `ValueError` moves from the build log to production requests.

  • What happens if a Django image uses ManifestStaticFilesStorage but collectstatic never ran for it?
    With `DEBUG = False`, every `{% static %}` lookup goes through `staticfiles.json`. There is no manifest, so the storage finds no entry and, because `manifest_strict` defaults to `True`, raises `ValueError: Missing staticfiles manifest entry for '...'`; pages that reference static files return a 500. With `DEBUG = True` the storage returns unhashed URLs, which is why the bug does not show up locally.
  • Why not run collectstatic when each container starts instead of at build time?
    It repeats identical work on every boot and slows scaling, it needs a writable `STATIC_ROOT` at runtime, and if several versions share one destination they overwrite each other's files and manifest. `ManifestStaticFilesStorage` also reads `staticfiles.json` once, when the storage is first created in a process, so a manifest rewritten after workers started is not picked up until they restart.

collectstatic is printing the menus before the restaurant opens: every copy is identical and made once from the recipes. migrate is rearranging the one shared kitchen: you do it once, before the new menu goes out, not once per waiter.

saying these in an interview costs you the question

  • Run migrate in the image build so the database is ready with the image.
  • collectstatic needs production database credentials to run.
  • Every web container can run migrate on start because Django locks the migration table.
  • It does not matter whether migrate runs before or after new containers take traffic.
  • Hashed static URLs working with DEBUG = True proves the production manifest is fine.