This episode splits the expense-tracker app’s single settings file into three environments — dev, staging, and production — and moves every secret into environment variables with django-environ. The result: the same codebase runs in all three environments, with no secrets left in code. Following episode 1’s production-readiness checklist, this is the first step in turning that checklist into reality.

Why Not One settings.py for Every Environment

When every environment shares one settings file, you end up scattering if DEBUG conditions everywhere, and a single oversight is enough to leave debug on in production. Splitting settings per environment lets each environment declare exactly what it needs, while the shared parts live in a single base file.

The Settings Directory Structure

Replace settings.py with a settings package placed next to manage.py, with one base file and one file per environment:

expense_project/
├── manage.py
└── expense_project/
├── __init__.py
├── settings/
│ ├── __init__.py
│ ├── base.py
│ ├── dev.py
│ ├── staging.py
│ └── production.py
└── urls.py

Install django-environ and Write settings/base.py

Install django-environ, then move the shared configuration into base.py. Note: SECRET_KEY has no real default value — if the environment variable is missing, the app fails immediately at startup instead of silently running with a fake secret.

# settings/base.py
import environ
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env()
environ.Env.read_env(BASE_DIR / ".env")
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env.bool("DJANGO_DEBUG", default=False)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[])
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"expenses",
]
DATABASES = {"default": env.db("DATABASE_URL")}

env.db() understands connection strings like postgres://user:pass@host:5432/dbname and returns the right dict for DATABASES, so switching databases is just a matter of changing one environment variable.

Settings for Dev, Staging, and Production

Each environment file only imports from base and overrides what differs. Dev enables debug and uses SQLite for speed; production disables debug and enables the security flags that episode 11 will cover in detail.

# settings/dev.py
from .base import * # noqa: F401,F403
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# settings/production.py
from .base import * # noqa: F401,F403
DEBUG = False
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

The .env File and the Never-Commit-Secrets Rule

Each environment has its own .env file, which never goes to Git. Add .env to .gitignore, and commit a .env.example containing only variable names and sample values. When you need to rotate a secret, just edit the .env file on the server and restart the process.

# .env.example — commit this file, do NOT commit the real .env
DJANGO_SECRET_KEY=thay-bang-secret-that
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgres://user:pass@localhost:5432/expense_prod

Running the App in Each Environment

Django picks settings through DJANGO_SETTINGS_MODULE, so there is no need to edit manage.py or wsgi.py per environment. Do a quick check with python manage.py check --deploy using production settings to scan security configuration before putting the app on a real server.

# Dev
DJANGO_SETTINGS_MODULE=expense_project.settings.dev python manage.py runserver
# Production
DJANGO_SETTINGS_MODULE=expense_project.settings.production gunicorn expense_project.wsgi:application

Apply It to Your Project

Start with the expense-tracker app: split the settings, move SECRET_KEY and DATABASE_URL into .env, then run check --deploy to see what is still missing. If the project still uses SQLite in production, do not rush to change databases yet — episode 4 will handle PostgreSQL; today, splitting the settings and cleaning secrets out of Git is enough to hit the goal.

Related Posts

FAQ

Should the .env file live on the production server?

Yes, but only on the server, readable only by the account running the app (file permission 600). Never commit .env to Git, never send it via chat or email, and never bake it into a Docker image — secrets are supplied to the container at runtime, which episode 16 will cover.

How do django-environ and python-decouple differ?

Both read environment variables, but django-environ adds env.db() and env.cache() to parse connection strings straight into Django configuration dicts. For just a few simple variables, python-decouple is enough; with a DATABASE_URL, django-environ is clearly more convenient.

What if I forget to set DJANGO_SETTINGS_MODULE?

Django falls back to the default settings in manage.py and wsgi.py — usually the old settings file you just deleted — so the app fails at startup. Set the variable in the systemd unit file or your process manager’s command, and verify with python manage.py check before every deploy.

References

  • django-environ documentation, Django settings documentation, The Twelve-Factor App (Config section).