Environment settings (after Django + uv)

Prerequisite: you finished django-uv.md — project runs, /api/health works, secrets still live in settings.py.

End state: SECRET_KEY, DEBUG, and ALLOWED_HOSTS come from a local .env file (not committed). settings.py reads them through the environment.

Suggested order among follow-ons: do this before Postgres, CORS, and Auth. Those steps all grow settings that should not be hard-coded. Second app does not depend on this.

How to use this: change one thing, then prove it still boots. Do not invent a settings framework beyond what is written here.


0. Why you are here

startproject puts a generated SECRET_KEY and DEBUG = True in source. That is fine for a private learning repo for an hour. It is not fine once the repo is shared, pushed, or deployed.

You are moving config that changes per machine out of git, not rewriting Django settings architecture.


1. Add django-environ

From the project root (where pyproject.toml is):

uv add django-environ
  • [ ] Run that.

Why this package: thin wrapper around env vars with typed helpers (env.bool, env.list) and optional .env file loading. Plain os.environ also works; django-environ saves boilerplate and pairs cleanly with a later DATABASE_URL.


2. Create .env (local only)

In the project root, create .env:

DJANGO_SECRET_KEY=replace-me-with-a-long-random-string
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1

Generate a better secret (paste the output into .env):

uv run python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
  • [ ] File exists; secret is not the placeholder string.

Confirm .gitignore already contains .env (from the base runbook). If not, add it now.

  • [ ] git status does not list .env as a tracked/new file you intend to commit (or it shows as ignored).

Why: .env is for your laptop. Production will inject the same variable names via the host (Railway, Fly, systemd, etc.) without that file.


3. Optional: commit a template

Create .env.example (this one is committed):

DJANGO_SECRET_KEY=
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
  • [ ] Save it.

Future you (or a teammate) copies it to .env and fills values. No secrets in the example file.


4. Teach settings.py to read the env

Near the top of config/settings.py, after the usual from pathlib import Path, add:

import environ

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

env = environ.Env(
    DJANGO_DEBUG=(bool, False),
    DJANGO_ALLOWED_HOSTS=(list, []),
)

environ.Env.read_env(BASE_DIR / ".env")

If BASE_DIR was already defined, do not duplicate it — only insert the environ bits around the existing BASE_DIR.

Replace the hard-coded settings:

SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env("DJANGO_DEBUG")
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS")
  • [ ] Remove the old literal SECRET_KEY = "django-insecure-…" and DEBUG = True / empty ALLOWED_HOSTS assignments you replaced.
  • [ ] Save.

Notes:

  • env("DJANGO_SECRET_KEY") with no default fails loudly if missing — what you want.
  • DJANGO_DEBUG=(bool, False) means missing/invalid debug defaults to False (safer than defaulting to True).
  • ALLOWED_HOSTS as a list reads comma-separated values from the env string.

5. Prove it

uv run python manage.py check
uv run python manage.py runserver
  • [ ] check reports no issues.
  • [ ] Server starts; /api/health still returns JSON.

Temporarily break .env (rename DJANGO_SECRET_KEY line) and run check again — you should get an error about a missing variable. Put it back.

  • [ ] Confirmed missing secret fails; restored .env works.

Checkpoint — you are done when

  • [ ] Secrets/debug/hosts are not hard-coded in settings.py
  • [ ] .env is gitignored; .env.example (if you made it) is committed without real secrets
  • [ ] App boots with values from .env

Stop. Do not add Redis/email/S3 settings “while you are here.” Add env keys when a later runbook needs them.


If you get stuck

Symptom Likely cause
ImproperlyConfigured / missing DJANGO_SECRET_KEY .env missing, wrong directory, or typo in the name
DEBUG always False Value not True/False as django-environ expects, or .env not loaded
DisallowedHost DJANGO_ALLOWED_HOSTS missing the host you used in the browser
.env shows up in git status as untracked to add Not listed in .gitignore

Next