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.pyimport environfrom pathlib import PathBASE_DIR = Path(__file__).resolve().parent.parent.parentenv = 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.pyfrom .base import * # noqa: F401,F403DEBUG = TrueALLOWED_HOSTS = ["localhost", "127.0.0.1"]DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", }}# settings/production.pyfrom .base import * # noqa: F401,F403DEBUG = FalseSECURE_SSL_REDIRECT = TrueSESSION_COOKIE_SECURE = TrueCSRF_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 .envDJANGO_SECRET_KEY=thay-bang-secret-thatDJANGO_DEBUG=FalseDJANGO_ALLOWED_HOSTS=example.com,www.example.comDATABASE_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.
# DevDJANGO_SETTINGS_MODULE=expense_project.settings.dev python manage.py runserver# ProductionDJANGO_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
- Django Production Operations #1: Series Introduction + Production-Readiness Checklist
- Flask: Managing Config per Environment (dev/staging/prod) Without Hardcoding SECRET_KEY
- FastAPI: Centralized Configuration with pydantic-settings
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).
