Dynamic Django Deployment

Lately I’ve noticed people posting various different takes on making the default Django settings a lot more dynamic. The development and deployment requirements for the projects I work on tend to be far from straight-forward and over time I’ve come up with my own approach to Django settings, so here it is.

The simplest approach typically involves importing all the names from a custom settings module at the end of the project’s standard settings module, providing the ability to override settings on a per machine basis.

try:
    from local_settings import *
except ImportError:
    pass

This still requires modifying local_settings.py on a per machine basis. Another approach that builds on this is to import a custom settings module from a host_settings package that has a unique name derived from the current machine the site is running on - this gives the advantage of being able to specify custom settings per machine and have each of these settings modules reside in the version control system, without the same file having to be modified on a per machine basis.

from socket import gethostname
try:
    exec ("from host_settings.%s import *" % 
        gethostname().replace("-", "_").replace(".", "_"))
except ImportError:
    pass

This simple version of the host_settings approach I’ve seen attempts to deal with the differences between a valid hostname and a valid module name with the two calls to replace, but ignores the fact a hostname could begin with a number which would be an invalid module name. Other versions of this approach handle this correctly and involve calling the __import__ built-in, iterating over and updating each name in the settings module individually, but once we look at some further requirements below and how to deal with them we’ll find this isn’t necessary.

Let’s take a step back for a moment and talk about some of the requirements I mentioned before. Where I work a project can end up deployed in a dozen different locations - a handful of developer machines and various different servers managing the project life cycle. Due to a variety of non-technical reasons it’s often required that various versions of a project run side by side in the same location, so with a project called project_x we end up with project_x_feature_a and project_x_feature_b sitting in the same location - all of a sudden all of our references to project_x are broken. So we end up taking the approach in our code that the actual name of a project’s directory is a moving target and should never be referenced - we never import from package_x and anything in our settings module that would typically reference this we set dynamically.

import os
project_path = os.path.dirname(os.path.abspath(__file__))
project_dir = project_path.split(os.sep)[-1]
MEDIA_URL = "/site_media/"
MEDIA_ROOT = os.path.join(project_path, MEDIA_URL.strip("/"))
TEMPLATE_DIRS = (os.path.join(project_path, "templates"),)
ADMIN_MEDIA_PREFIX = "/media/"
ROOT_URLCONF = "%s.urls" % project_dir

So that removes any hard-coded reference to the project’s directory name, however we sometimes have to go as far as having host specific settings that vary across these different project versions residing on the same machine, such as a different database for example. The ultimate goal here is to not have any files in the project’s version control repository that are manually edited for a specific instance of the project. So using the host_settings approach from earlier on, we continue on from the code above by using the project_dir variable when referencing the machine specific host_settings module so that each of the host_settings modules are named not only after the machine they exist for, but the project directory as well.

from socket import gethostname
host_settings_module = "%s_%s" % (project_dir, 
    gethostname().replace("-", "_").replace(".", "_").lower())
host_settings_path = os.path.join(project_path, "host_settings", 
    "%s.py" % host_settings_module)
if not os.path.exists(host_settings_path):
    try:
        f = open(host_settings_path, "w")
        f.close()
    except IOError:
        print "couldn't create host_settings module: %s " % host_settings_path
try:
    exec "from host_settings.%s import *" % host_settings_module
except ImportError:
    pass
TEMPLATE_DEBUG = DEBUG

As an added bonus, we try to create the host_settings module if it’s missing and warn if we’re unable to create it.