elias Published July 17, 2021 · 4 min read

Managing multiple configuration environments in Django

A decorative hero image for this page.

When working on a sufficiently large Django project that is expected to be developed for a while, the need for managing configurations across different environments quickly becomes apparent. In this post we discuss how these differences can be managed so that repetition is minimal. We leverage Python's own module and import system to implement cascading configurations without introducing new project dependencies and without breaking Django's conf.settings functionality.

devops django

The objective of this article is to get a Django application to change its configuration depending on the environment it is being run in (production vs staging vs local or development). We will also make sure to follow best practices when dealing with configuration secrets (think API keys) and try to keep our config as DRY as possible. We start by figuring out how the Django configuration system works and what are its moving parts and build from the tools it gives us to get to a point where we can switch settings by chanigng the value of an environment variable.

Standard configuration

When creating a Django project with django-admin startproject awesome_app we get the following folder structure

.
├── awesome_app
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

The awesome_app/settings.py file contains the project's configuration. Django knows which file contains the project's configuration by reading the DJANGO_SETTINGS_MODULE environment variable, which defaults to awesome_app.settings. As you might have guessed, the value of this variable is expected to be the name of a Python module.

This file configures how Django connnects to your database, which applications are loaded and more via plain Python variables. Here's a full list of the settings Django accepts. As an example, whether timezone support is enabled is determined by the variable USE_TZ which is initially set to True when django-admin generates your project. Moreover, it is possible to read these variables yourself and change how your app behaves. The recommended way to do this is by importing django.conf.settings and reading the variables from there, instead of hard-coding the name of your app, since this makes your code more reusable.

These variables do not have to be static values, you could perhaps read them from the environment via os.environ more or less like so:

import os

USE_TZ = os.environ.get('USE_TZ', True)

This is especially useful for making sure secrets don't get checked into version control, e.g. if we were connecting to a database which is password protected, we could retrieve the password from an environment variable instead of directly assigning it to DATABASES['default']['PASSWORD']. In this way, only the people responsible for deploying our applcation have access to that password. Incidentally, one way to set these environment variables is by export-ing them if you are running your app from a shell, by setting them in a docker-compose file or by using something like django-dotenv to automatically read them from a file .env which should be excluded from version control.

Using multiple modules for different environments

The previous approach of reading values from the environment can certainly be used to configure an app to run differently depending on whether it is in, say, production or development. However, not all your settings need to be secret, you might want to check some of them into version control, and developers using Windows do not particularly like messing with files starting with ".". However, since Django expects a Python module as the source of your configuration, we can just copy the configuration to separate files for each environment we want to support and set the DJANGO_SETTINGS_MODULE environment variable (plus any other variables holding secrets).

To do this, create a folder named settings and a file __init__.py inside it (to turn it into a Python module). Then move your settings.py file to settings/common.py. This leaves us with the following directory structure:

.
├── awesome_app
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings
│   │   ├── common.py
│   │   ├── development.py
│   │   ├── __init__.py
│   │   ├── production.py
│   │   └── staging.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Notice how we also added development.py, staging.py and production.py to our settings folder. This files just contain the line from .common import * and any variables which we want to override from the common.py file. For example, if we want to change the database host and disabled debugging in production we would put this in production.py:

from .common import *

DATABASE['default']['HOST'] = '1.2.3.4'

DEBUG = False

In this way, we can keep all of our settings together in common.py and override just what we need in each environment. Remember to continue using environment values for secret settigns such as API keys, since those should not be checked into version control.

Finally, to let Django know which settings to use, remember to set the DJANGO_SETTINGS_MODULE environment variable before running your application. On POSIX systems this can be done with

export DJANGO_SETTINGS_MODULE=awesome_app.settings.development

Ready to bring your vision to life?

Get in touch