skip to content

Why can a Django collectstatic step fail during an image build, and how do you make the settings importable there without production secrets?

level: middleimportance: should knowfreq 38%

answer

  1. what django.setup() imports
  2. environment lookups at import time
  3. ready() hooks and the database
  4. same STORAGES at build and runtime

basics

~20 s

collectstatic imports the whole settings module and every installed app, so settings that demand runtime secrets, or ready() hooks that query the database, break the build. Give that command placeholder values, keep production's STORAGES backend, and keep queries out of import time.

solid answer

~40 s

Before `collectstatic` copies anything, Django imports the settings module, populates `INSTALLED_APPS` and calls each `AppConfig.ready()`. So `os.environ["DJANGO_SECRET_KEY"]` in settings raises `KeyError` at build, and a `ready()` that queries fails because there is no database. Django itself raises `ImproperlyConfigured` for an empty `SECRET_KEY` only when something reads `settings.SECRET_KEY`, and defining `DATABASES` opens no connection, so a throwaway secret passed to that single command is enough. Keep optional settings on `os.environ.get()` with defaults, move queries out of import time, and build with the same `STORAGES["staticfiles"]` backend, `STATIC_ROOT` and `INSTALLED_APPS` as production; a build that uses plain `StaticFilesStorage` writes no manifest, and production's `ManifestStaticFilesStorage` then fails every `{% static %}` lookup.

code

python · 28 lines
python
# settings.py (one module for every environment)
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # required; the build passes a placeholder
DEBUG = os.environ.get("DJANGO_DEBUG") == "1"
ALLOWED_HOSTS = [h for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("DB_NAME", "app"),
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "USER": os.environ.get("DB_USER", "app"),
        "PASSWORD": os.environ.get("DB_PASSWORD", ""),
    }
}  # no connection is opened here

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
    },
}

go deeper

for a junior

Remember that manage.py commands import your whole settings module and installed apps first, so a missing environment variable breaks them before they do any work.

for a middle

Explain the boot sequence before collectstatic runs, why DATABASES and an empty SECRET_KEY do not fail by themselves, and why build and runtime must share STORAGES.

for a senior

Diagnose a failing build from its log, move queries out of ready(), and refuse shortcuts like --no-post-process or secrets stored in the image.

for a principal

Set the policy: one environment-driven settings module, immutable images, and a build that fails on broken static references rather than shipping them.

## What collectstatic loads before it copies a single file `python manage.py collectstatic` is an ordinary Django management command, so before its own code runs Django boots the project the same way it does for a web process: 1. `manage.py` sets `DJANGO_SETTINGS_MODULE` with `os.environ.setdefault` (an existing value wins), and Django **imports the settings module**, executing every line of it. 2. `django.setup()` populates the app registry from `INSTALLED_APPS`, importing each app's models, and calls every `AppConfig.ready()`. 3. The command runs its system checks; for `collectstatic` these are only the checks tagged `staticfiles`, not the database ones. 4. Only then does it find and copy the files and, with `ManifestStaticFilesStorage`, hash them and write `staticfiles.json`. An image build is an unusual environment for steps 1 and 2: no production database, usually no secrets, often no network. Code that assumes those at import time breaks the build even though copying files needs none of them. ## The usual failures and their fixes | Symptom in the build log | Cause | Fix | |---|---|---| | `KeyError: 'DJANGO_SECRET_KEY'` | settings read a required environment variable at import | pass a throwaway value to that one command | | a database connection error | an `AppConfig.ready()` or module-level code runs a query | move the query into a view, command or task | | `ImproperlyConfigured: The SECRET_KEY setting must not be empty.` | code that runs at import read an empty `settings.SECRET_KEY` | provide a placeholder for the build step | | `Post-processing 'css/site.css' failed!`, then `The file '...' could not be found` | a stylesheet references a file that does not exist | fix the reference; the build is right to fail | Two Django facts make the fixes cheap: - **Defining `DATABASES` opens no connection.** Django connects on first use, so a settings module can describe the production database without the build ever reaching it. - **An empty `SECRET_KEY` fails only when read.** Django raises `ImproperlyConfigured` when something accesses `settings.SECRET_KEY`, not when the module is imported. `collectstatic` signs nothing, so a placeholder satisfies settings code that insists the variable exists. ## Making settings build-safe - **Required secrets stay required at runtime**, but the build passes a placeholder to the single `collectstatic` invocation instead of storing a value in the image. - **Optional values get defaults** through `os.environ.get(...)`, so a missing variable yields a harmless value rather than an import error. - **No database work at import time.** Module-level queries and queries inside `ready()` fail at build, and they also run in every worker at start, which is a problem of its own. - **Keep production's storage backend.** The build must use the same `STORAGES["staticfiles"]` backend as runtime. A build that falls back to a development settings module with plain `StaticFilesStorage` writes no manifest, and production, configured for `ManifestStaticFilesStorage`, then raises `ValueError: Missing staticfiles manifest entry` for every `{% static %}` tag when `DEBUG = False`. - **Keep the same `STATIC_ROOT` and `INSTALLED_APPS`**, so the collected set matches what production resolves. ## Environment-specific settings without a second codebase The common pattern is **one settings module whose values come from the environment**: the same image runs in every environment, and the build step differs only in the variables it is given. A separate build module chosen with `--settings` or `DJANGO_SETTINGS_MODULE` also works, provided it imports the production module and overrides only secrets. A module that diverges in `INSTALLED_APPS`, `STATIC_ROOT` or `STORAGES` produces an image whose collected files do not match what the runtime expects. The same applies to anything that changes which files exist: an app that is only installed in development, or a `STATICFILES_DIRS` entry that points at a directory the build never produces, changes the collected set. How settings are split and loaded in general is a separate subject; the constraint here is that build and runtime agree on everything `collectstatic` reads. ## Why a loud build failure is the good outcome Every failure above is cheaper at build time than at runtime: the image never ships, nothing is serving, and the log names the file. The tempting shortcut, `collectstatic --no-post-process`, makes the build pass by skipping hashing; in a fresh build directory that leaves no hashed files and no manifest, which moves the same failure to production requests. Likewise, storing the real `SECRET_KEY` in the image so every step "just works" trades a build error for a leaked credential in every copy of the image.

  • Why is a placeholder SECRET_KEY safe for the Django collectstatic build step?
    `collectstatic` signs nothing, so the key's value never affects its output. The placeholder exists only for that command, and at runtime the environment supplies the real key. What is not safe is the opposite shortcut, storing the real key in the image, because every copy of the image then carries the credential.
  • Is a separate build-only Django settings module a good idea?
    It can work, via `--settings` or `DJANGO_SETTINGS_MODULE`, if it imports the production module and overrides only secrets. It goes wrong when it diverges: a different `STORAGES` backend writes no manifest, and a different `INSTALLED_APPS` or `STATIC_ROOT` collects a set of files production does not expect. Many teams prefer one env-driven module.

saying these in an interview costs you the question

  • collectstatic connects to the database, so the build needs production credentials.
  • Django refuses to import settings at all unless SECRET_KEY is non-empty.
  • Build with plain StaticFilesStorage; production's manifest storage will hash files on first request.
  • Store the real SECRET_KEY in the image so every build step works.
  • Add --no-post-process to get past a post-processing error in the production build.