Deploying Django Sites Using the 12-Factor Pattern: Flexibility, Portability, and Maintainability

Better environment configuration, inspired by Heroku, borrowed from Foreman.

The twelve-factor application pattern, published by one of Heroku’s co-founders, define a methodology for developing and deploying web applications, specifically software-as-a-service.

These are the twelve in brief. More detail is available on the website.

  1. Codebase One codebase tracked in revision control, many deploys
  2. Dependencies Explicitly declare and isolate dependencies
  3. Config Store config in the environment
  4. Backing Services Treat backing services as attached resources
  5. Build, release, run Strictly separate build and run stages
  6. Processes Execute the app as one or more stateless processes
  7. Port binding Export services via port binding
  8. Concurrency Scale out via the process model
  9. Disposability Maximize robustness with fast startup and graceful shutdown
  10. Dev/prod parity Keep development, staging, and production as similar as possible
  11. Logs Treat logs as event streams
  12. Admin processes Run admin/management tasks as one-off processes

If you’ve used Heroku then you’re probably quite familiar with these factors as constraints. The biggest difference approaching Heroku deployment with traditional Django development is that, uh, there’s no way you can adequately track configuration values like database credentials in your settings, and you sure as hell can’t resort to a local_settings file. And that’s not a bad thing.

Configuration

In the twelve-factor model, all configuraiton variables are stored in the OS environment. This makes updating settings in the deployed application way easier but what to do in local development?

The first solution is to use Foreman, a Ruby application that manages Procfile based processes. Your Procfile is just a template of process commands and it’s what Heroku uses to identify what needs to run in your app. The local environment variable issue is solved by including a .env file in your project directory (excluded from version control) into which you can declare your own local configuration settings.

The upside is that everytime you run foreman start or foreman run python manage.py migrate you’re reading in those variables. The downside is that for running management commands it adds a few extra keystrokes. Not to mention that you lose the devserver’s reload on code change capability. Nobody wants to stop and start and stop and start processes manually.

Be your own head Honcho

So let’s just update the Django project’s manage.py file. What we want to do is read the faux environment variables into Python’s os.environ dictionary. Here we’ll take a cue, and some code, from Honcho. Honcho is a Python port of Foreman written by Nick Stenning and distributed using the MIT license. All that we want to accomplish is getting configuration values from our own .env file.

Here’s the default Django manage.py file:

The updated manage.py code just lifts Nick’s read_env function and executes it whenever a command is issued using the manage.py file.

Of course if you’re using Honcho then you could simplify your manage.py file by simply importing the function.

Updating settings.py

The next step is to add these values to your .env file and then update your settings file to get them. Here’s a small sample .env file:

DATABASE_URL='postgres://dbuser:[email protected]/mydb'
DEBUG=False
FACEBOOK_APP_ID='51821095311150'
FACEBOOK_API_SECRET='fj43dfe897f612cf3448ce34f0f6ad1a1'

And in your settings file, instead of:

DEBUG = True
TEMPLATE_DEBUG = DEBUG

Update to this:

def bool_env(val):
    """Replaces string based environment values with Python booleans"""
    return True if os.environ.get(val, False) == 'True' else False

DEBUG = bool_env('DEBUG')
TEMPLATE_DEBUG = DEBUG

(Thanks to @emperorcezar for pointing this in a more concise direction)

False is a more sensible default to avoid accidentally exposing debug data. The new settings code calls a small helper function bool because the value returned will be a string saying ‘True’ or ‘False’ and we want to work with boolean values. Any place you have a setting that might change between environments, just ensure that the value is set by a default value after checking the os.environ dictionary.

Update (January 2013)

It’s very important to be able to handle default values for your settings, and to provide sensible defaults. In the example above we shared a tiny helper method that picks out boolean settings from the environment. In practice, we’ve found it simpler to accommodate non-Boolean values, too, in the settings helper function.

def env_var(key, default=None):
    """Retrieves env vars and makes Python boolean replacements"""
    val = os.environ.get(key, default)
    if val == 'True':
        val = True
    elif val == 'False':
        val = False
    return val

Now you can replace any environmentally configured setting type.

DEBUG = env_var('DEBUG', False)
LOGGING_DIR = env_var('LOGGING_DIR', '/var/log/myproject')
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'elasticstack.backends.ConfigurableElasticSearchEngine',
        'URL': env_var('HAYSTACK_URL', 'http://127.0.0.1:9200/'),
        'INDEX_NAME': 'haystack',
    },
}