Quick Tips for Django and Heroku, Static Files and Multiple Environments

For a quick publicly facing Django prototype I decided to use Heroku.com. For low use, it is free, and they have plenty of good docs to get you started, and masses of add-ons, many of which have free tiers as well.

https://devcenter.heroku.com/articles/django is really all that is needed to get you out of the gate and have something running. This guide walks you through setting up a local environment, using 'pip install' to add a few dependencies needed for Heroku, committing the code to git, and then pushing the code to the Heroku remote, which triggers installing and launching the app.

Instead of reinventing the wheel, I will only highlight some issues I had with that initial setup and how I got around them.

Static File Serving

The Django manage.py runserver command explicitly states that it shouldn't be used in a production environment, and that it should be used to serve static files. Also, Heroku doesn't not allow for static file usage, instead pointing you to S3 or other CDN type services for static files. But for a prototype, I thought that was overkill, so I checked around StackOverflow and other sites for the way to get Heroku to serve Django files for the Heroku filesystem.

To get this to work takes some adjustments in three different places, the settings.py file, the main urls.py file and your Procfile that launches the app in Heroku.

First, you need to setup your Static Files location in a way that works for both your local environment and on Heroku (trying to avoid branching configs per environment).

Generally, there is a "#STATIC_ROOT = ''" type entry in the settings.py file, that more or less says collect all the static files and place them 'somewhere'. Well, in Heroku, this isn't good, as that somewhere can't be absolutely defined, and it would break your local development. Below is the first step, which uses asks the os where to place things relative to the settings.py file.

import os
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
STATIC_ROOT = os.path.join(PROJECT_ROOT,'staticfiles/')
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'

Then in the urls.py file add this entry:

urlpatterns += patterns('',
    (r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT}),
)

Lastly, you need to tell Heroku to actually collect all the static files. The regular 'manage.py runserver' command does this for you in development, but if you followed the above guide you setup a gunicorn process to handle your web requests, and it knows nothing about collecting static files for Django.

In your procfile, replace the entry you have which probably looks like this:

web: gunicorn hellodjango.wsgi

With this:

web: python manage.py collectstatic --noinput; gunicorn hellodjango.wsgi

This runs the collectstic command from the manage.py script, and then launches the gunicorn process.

Now you have a simple static file server bases inside Heroku. It really isn't a good setup for a scalable production environment, but for a quick prototype, it is nice not to have to hook into, and maybe pay for, a CDN or external file system.

One great tip I liked from the Two Scoops of Django book is that you should try to keep your settings.py as environment agnostic as possible, and that means setting and using environmental variables liberally. There are many help pages for settings such variables in Windows or on a Mac, so I don't want to cover that here, but I will touch on how to use them in your Django settings.py file and how to set them in Heroku.

Borrowing from Two Scoops of Django, place this at the top of your settings.py file.

import os
import dj_database_url

from django.core.exceptions import ImproperlyConfigured

def get_env_variable(var_name):
    """ Get the environment variable or return exception """
    try:
        return os.environ[var_name]
    except KeyError:
        error_msg = "Set the %s environment variable" % var_name
        raise ImproperlyConfigured(error_msg)

It imports a few needed libraries, as well as adds a helper function we will use in other settings.

For most developers, there is a distinct set of settings that are specific for local development, so I decided to add a LOCAL_DEV variable, that lives along side of the DEBUG one that is used by Django.

DEBUG = get_env_variable("DJANGO_DEBUG")
if DEBUG == 1 or DEBUG == '1':
    DEBUG = True
else:
    DEBUG = False
    
LOCAL_DEV = get_env_variable("DJANGO_LOCAL_DEV")
if LOCAL_DEV == 1 or LOCAL_DEV == '1':
    LOCAL_DEV = True
else:
    LOCAL_DEV = False

if DEBUG:
    DEBUG_TOOLBAR_PANELS = ( ...settings...   )
    #...other DEBUG settings ....

DATABASES = {}

if LOCAL_DEV:
    ALLOWED_HOSTS = ['*'] #useful when testing with DEBUG = FALSE
    INTERNAL_IPS = ('127.0.0.1',) #sets local IPS needed for DEBUG_TOOLBAR and other items.

    DATABASES = {
        'default': {
            
            'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
            #'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
            'NAME': 'hellodjango',                      # Or path to database file if using sqlite3.
            #'NAME': '/Users/SomeUser/Documents/Django Projects/hellodjango/hellodjango.sql',                      # Or path to database file if using sqlite3.
            # The following settings are not used with sqlite3:
            'USER': '',
            'PASSWORD': '',
            'HOST': 'localhost',                      # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
            'PORT': '',                      # Set to empty string for default.
        }
    }
else:
    # Parse database configuration from $DATABASE_URL
    DATABASES['default'] =  dj_database_url.config()
    
    # Honor the 'X-Forwarded-Proto' header for request.is_secure()
    SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

First you might ask, why are those two settings coming in as 0 and 1, instead of True and False. Answer, in Heroku, all the settings are saved as strings, and the string 'False' will return as True, which caused me an hour or two of pain. So I check for the 1 and 0 and change to True and False.

This setup allows for the local development to use a local database, while Heroku uses the DB url through the 'dj_database_url.config()' call to find its database somewhere in the clouds.

Lastly, the writers suggest that the SECRET_KEY be stored as a variable, that way it is never in your source code. I also updated the DEFAULT_FROM_EMAIL, that way I could adjust it without needed to relaunch the Heroku app.

DEFAULT_FROM_EMAIL = get_env_variable("DJANGO_EMAIL_DEFAULT_USER")

SECRET_KEY = get_env_variable("DJANGO_SECRET_KEY")

But we still haven't actually set these variables in Heroku. From the root of your project on your system, you can use the 'heroku config' command to handle it.

$ heroku config
DATABASE_URL:     postgres://....amazonaws.com:....
HEROKU_POSTGRESQL_CYAN_URL:     postgres://....amazonaws.com:....

Since you haven't set any yet, the created DB variables are all that is present. To add a variable use the 'heroku config:add VARNAME=stuff'

$ heroku config:add DJANGO_EMAIL_DEFAULT_USER=from@sample.com
heroku config:add DJANGO_SECRET_KEY=something_secret
heroku config:add DJANGO_DEBUG=1
heroku config:add DJANGO_LOCAL_DEV=0

Now, all the settings are loaded, and you can check again:

$ heroku config
DATABASE_URL:     postgres://....amazonaws.com:....
DJANGO_EMAIL_DEFAULT_USER:  from@sample.com
DJANGO_LOCAL_DEV:           0
DJANGO_DEBUG:               1
DJANGO_SECRET_KEY:          something_secret
HEROKU_POSTGRESQL_CYAN_URL:     postgres://....amazonaws.com:....

Multiple Environments for Apps

Lastly, a couple quick tips regarding having a staging and production environment on Heroku for your app. Since mostly likely everything you are doing thus far is free, double free is still free, so why not spin up a staging server, to both test new settings and make sure you don't break anything just as a client is playing with your prototype.

https://devcenter.heroku.com/articles/multiple-environments has most of the tips you need. Once getting through the article you can now issue heroku commands and append the '--remote staging' to target the staging server, instead of the main production one.

I never liked the 'git push heroku master' command, as it just didn't feel informative enough. So let's first rename that to 'production':

git remote rename heroku production

Now you would run 'git push production master' and 'git push staging master' to push code to the Heroku apps.

Lastly, make sure you run this command, so that any 'heroku' commands that you run without a --remote flag will go to the staging server, instead of production (making it a bit more accident proof).

$ git config heroku.remote staging

Well, this has become a lengthy article, and there are still a few other items I might mention, but I will hold them off to another article. Mostly they had to do with deploying Django generically, not specifically tied to Heroku.