Django and Heroku – getting it working
What follows is based on a short talk I gave to the New Zealand Python User Group in Feb 2015. This blog post provides some specifics on areas I was only able to hand wave over during the talk.
I’d previously used Heroku for a Ruby on Rails project and remembered it being very straightforward so I was surprised to find it wasn’t that great an experience. The documentation is fragmentary and seems to have been only partially updated to reflect changes in Django and the Heroku environment.
In the end I decided to suspend my original project and try to make the simplest possible Django project work on Heroku. For “simplest possible” I chose the “Polls” project from the Django Tutorial . I got it working and the code is available in my github account: https://github.com/shearichard/polls17/tree/v2.0 . If you’re interested the version of the Project which works locally and before I made any changes to support use on Heroku is here : https://github.com/shearichard/polls17/tree/v1.0 .
What needed to be done
To complement the Heroku documentation I’m going to record here the changes that were made to the Project between v1.0 (working locally) and v2.0 (working on Heroku).
The files to which changes were applied to support use in Heroku are as follows :
mysite/mysite/settings.py (before and after)
mysite/mysite/settings_heroku.py (after – there was no ‘before’ for this file !)
mysite/mysite/wsgi.py (before and after)
requirements.txt (before and after)
diff --git a/mysite/mysite/settings.py b/mysite/mysite/settings.py index cb992c1..b2082ba 100644 --- a/mysite/mysite/settings.py +++ b/mysite/mysite/settings.py @@ -87,4 +87,5 @@ USE_TZ = True # https://docs.djangoproject.com/en/1.7/howto/static-files/ STATIC_URL = '/static/' STATIC_ROOT = 'staticfiles' TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
The settings_heroku.py file was completely new for use within the Heroku environment and we can see it referenced below from within wsgi.py when a test is made to see if the code is running within Heroku.
The final form of settings_heroku.py is as follows :
from .settings import * import dj_database_url DATABASES['default'] = dj_database_url.config() BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STATIC_ROOT = 'staticfiles' STATIC_URL = '/static/' STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static'), ) # Simplified static file serving. # https://warehouse.python.org/project/whitenoise/ STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage'
Things worthy of note here are :
- we import the whole of the local settings file (referenced here as ‘.settings’) and then change or add to it as necessary.
- we make use of the dj-database-url to pick up the database configuration to be used in the Heroku environment
- `STATIC_ROOT` and `STATICFILES_DIRS` are not needed in the standard version of the ‘Polls’ project but they are needed when we move to Heroku so they’re added here.
- `STATIC_URL` is already defined in the standard settings file and so doesn’t actually need to be in settings_heroku.py at all.
- STATICFILES_STORAGE allow for the use of Whitenoise a module which allows wsgi apps (such as this one) to serve their own static files, something which hadn’t previously been possible. There’s other good reasons to use Whitenoise in the areas of file compression and cache-header handling
The version of wsgi.py before the changes for Heroku is very straightforward and can be seen below.
""" WSGI config for mysite project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") from django.core.wsgi import get_wsgi_application application = get_wsgi_application()
To make wsgi.py work for Heroku there are essentially three changes:
- Make the settings file used dependent on the existence of an environmental variable, ‘DYNO’. If it’s present then the code is running on Heroku and the server is started using the the settings_heroku.py file shown above, otherwise we continue to use the settings.py file.
- To make use of Whitenoise we take the the output of `get_wsgi_application` and use it as an argument when instantiating a `DjangoWhiteNoise` object.
- Lastly, and least important, we redirect standard output so to standard error. This isn’t necessary at all and is something I did to make for easier diagnosis of issues while getting the Heroku specific version working.
diff --git a/mysite/mysite/wsgi.py b/mysite/mysite/wsgi.py index 15c7d49..e5e1e5c 100644 --- a/mysite/mysite/wsgi.py +++ b/mysite/mysite/wsgi.py @@ -8,7 +8,20 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os import sys #Allows us to see useful stuff in Gunicorn output sys.stdout = sys.stderr #Rely upon env var 'DYNO` to determine if we are #running within Heroku if 'DYNO' in os.environ: os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings_heroku") else: os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") from django.core.wsgi import get_wsgi_application from whitenoise.django import DjangoWhiteNoise application = get_wsgi_application() application = DjangoWhiteNoise(application)
The requirements.txt (created as the output from a `pip freeze` command) reflects the libraries installed at any given point.
Here’s the diff of requirements.txt between the local installation and the ‘Heroku’ ready installation.
As can be seen the extra libraries required by the migration to Heroku were :
diff --git a/requirements.txt b/requirements.txt index 98b2fd1..4e189d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Django==1.7.4 Pygments==2.0.2 argparse==1.2.1 dj-database-url==0.3.0 django-extensions==1.5.0 django-pdb==0.4.1 fancycompleter==0.4 gunicorn==19.2.1 @@ -11,5 +12,6 @@ psycopg2==2.6 pyflakes==0.8.1 pyrepl==0.8.4 six==1.9.0 whitenoise==1.0.6 wmctrl==0.1 wsgiref==0.1.2
A general point about Project structure
A good deal of the Heroku documention assumes that your project directory (the one that contains manage.py) is also your root directory . This isn’t how I do things. I prefer my root directory to contain stuff like .gitignore, requirements.txt, README.md etc and to have a directory within the root which is my project directory.
If your project is similarly structured it’s worth bearing in mind that the Procfile required by Heroku should include ” –pythonpath ./mysite” (where ‘mysite’ is the name of your project directory) as an argument to the gunicorn invocation … I had a number of issues before I did this . Here’s an example of the argument in use.
A general point about the Heroku CLI
heroku ps --app foo
Provides a list of running dynos in your ‘foo’ application.
Anyway the strange thing is that it seems to me that almost every command you issue via the Heroku CLI requires the
argument, where ‘foo’ is the name of your application, and yet the documentation never mentions that ! You work it out pretty quickly because you don’t do much without without it but it’s strange all the same.
Using the free levels of Heroku for running a Django project gives you access to a really high quality hosting environment at a very attractive price (free as long as you don’t get too much traffic or data). Once you’ve got over the bumps it works really well and for many people will be a good solution for hobby projects.