9 Straightforward Steps for Deploying Your Django App with Heroku
Even though I’ve deployed a few Django apps using Heroku, I’ve approached each new one with a fresh trepidation that feels like going to the dentist for the first time in a couple of years and dreading bad news. The goal of this post is to share some of my learnings that aren’t clearly represented in the official documentation to spare you the angst next time.
Getting started
This is the second blog post in a two part series — the first covered how to add a Postgres database to your Django app.
Using the sample app
Because I learn best by studying working examples, I’ve created one to go with this blog post; it builds on my previous post about adding Postgres to your Django app. Note that you need to use something other than SQLite on Heroku or face having your entire database wiped out every 24 hours. So, not ideal.
The code is on Github; the deployed site is here. The example code uses Django 1.11.1, Python 3.5, PostgreSQL 9, and a free Heroku hobby dyno (their name for a Linux container in which your app will run). We will also add gunicorn 19.7.1, whitenoise 3.3.0, and dj-database-url 0.4.2 to the mix in the steps below, and these are all included in the requirements.txt file.
To deploy the sample app on your own, you will need to:
- Create and activate a virtual environment
- Install the project requirements
pip install -r requirements.txt
- Create and source environment variables in the virtual environment
- Create a Heroku account and install the Heroku CLI (Command Line Interface)
- Migrate the database with these commands in the terminal:
python manage.py makemigrations
python manage.py migrate
Getting started
This process has 9 steps, which might sound daunting, but if you’ve gotten stuck on this before, I think these are broken down enough to be clear and self-contained.
For the Heroku parts, examples will be in their CLI (command line interface), but the steps can also be completed in the Heroku dashboard if you prefer.
Let’s go!
Step 1: Add a Procfile
The Procfile (compressed version of “process file”) tells the dyno (Linux container that Heroku creates for you) what what to do with your app. You can add multiple process types to your Procfile, but in this post I’ll focus on running your Django app as a website.
In your project root, create a file called Procfile
(capital “P”, no extensions) and add the following line of code to it:
web: gunicorn sampledeploy.wsgi —-log-file -
To break this down:
web: gunicorn
is telling your dyno to run a web process with gunicorn.django-heroku.wsgi
is telling the dyno to look in a file calledwsgi
in the folder calledsampledeploy
— substitute this with the name of your project.—-log-file -
is saying add the output to the logs
gunicorn
is an abbreviation for “Green Unicorn” and it’s a sort of translator/courier that sits between the server and your web app when there’s a need to show dynamic content. When the server gets a request from the end user’s browser that involves some dynamic content, it calls gunicorn, which takes HTTP packets from the server, translates them into a Web Service Gateway interface (that’s the WSGI!) readable format, and fires it over to your web framework (Django) to process. When it’s ready, the framework sends an HTTP request back to the server via gunicorn, and the server sends that data back to the end user’s browser.
WSGI is a standard communication spec for enabling web servers and applications to communicate with one another. This article is a great intro.
Your Procfile
won’t work unless you’ve installed gunicorn via the requirements.txt
file in the sample app or pip install gunicorn
in your terminal.
Step 2: Try running the app locally via Heroku
Heroku recommends this step to make sure everything is in order with the Procfile and the requirements locally before you try to deploy.
To avoid some common issues before you begin, make sure that:
- Both the
requirements.txt
file and theProcfile
are in your project root, the same directory asmanage.py
. If they are not, the paths get messed up and you’ll get an error saying your app can’t be found. - You go into your
settings.py
file and update theALLOWED_HOSTS
variable from an empty list to `ALLOWED_HOSTS = [‘0.0.0.0’, ‘localhost’]
. While the variable is generated by Django as an empty list when you start your project and it works fine in development, Heroku is (likely) attempting to run your app locally on a different port than you did when running your local server — adding the items to the list above keeps the app working when running locally with heroku (port 5000) and on localhost (port 8000). We’ll come back toALLOWED_HOSTS
in a moment.
You might notice a warning that looks like this in the terminal:
[WARN] No ENV file found
We will take care of this in a moment as well.
Step 2: Creating a heroku app
In your terminal, sign into Heroku by typing heroku login
and your credentials.
Now we’re going to create our Heroku app! Type heroku create
, and like magic, you get this output with a delightful autogenerated app name.
Our app will eventually live at the url in blue. Heroku doesn’t have any of our actual files yet, so if you go there now, you will get the placeholder page you see on the left.
This is where I recommend going back to settings.py
and adding the Heroku URL to our list of ALLOWED_HOSTS
like this: ALLOWED_HOSTS = [‘0.0.0.0’, ‘localhost’, ‘stark-escarpment-63219.herokuapp.com’]
.
Now, let’s give our app files to Heroku.
Step 3: Add a heroku remote
A nice feature of Heroku is that you can deploy your code using git. Recall our terminal output when we created our app in Heroku:
The text in green is the URL for our Heroku git repo. To get our files to Heroku we’ll run a command, git push heroku master
but if you do that now, you’ll get this error:
Let’s add the remote, like so: git remote add heroku https://git.heroku.com/stark-escarpment-63219.git
. Then type git remote -v
to verify that your remote has been added.
We can push to Heroku now, but this is sort of a tricky moment, because there are small configuration steps at this point with a Django app that will prevent deployment if you don’t take them.
Let’s tackle these one at a time.
Step 4: Set environment variables in Heroku
This is one of the easiest problems to spot. If you push to Heroku now, this is one error you will see in the terminal.
django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.
This error is specifically addressing the SECRET_KEY
variable in your settings.py
file — remember you’ve only set the environment variables in your local dev environment but not in Heroku. So, in the terminal, type:
heroku config:set DJANGO_SECRET_KEY=`thisismysecretkey`
Where DJANGO_SECRET_KEY
is the name of your env variable in the settings.py
file and `thisismysecretkey`
is your actual secret key.
At this point, it’s a good idea to add any other environment variables you were using locally.
Step 5: Setting the STATIC_ROOT
Here’s another error you’d see if you tried to push to Heroku now:
django.core.exceptions.ImproperlyConfigured: You’re using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path.
This is a pretty straightforward fix. Heroku is trying to find your staticfiles
and has no idea where to look. We’ll need to do more with static files in a later step, but for now, we’ll focus on getting things running.
Head over to your settings.py
file. There’s no STATIC_ROOT
variable! Let’s fix that. Add the following to your settings.py
:
STATIC_ROOT = os.path.join(BASE_DIR, ‘staticfiles’)
Step 6: Push to Heroku!
Finally! After all of the steps above, this is as simple as typing git push heroku master
…sometimes.
When deploying with Git, Heroku really wants you to initiate the repo from the project root, where your manage.py
lives. You might notice in the sample app repo that manage.py
is one level down.
Even though running the app locally with Heroku worked, if you try to deploy with git push heroku master
you’ll see this error:
If you Google this, you’ll see some advice on Stack Overflow to just delete your git folder and reinitiate it, but doesn’t make it totally clear that:
- You will lose all of your git history
- If you reinitiate in the same place, it solves nothing
So, assuming that you don’t want to lose your git history, and you’ve set up your repo as I have, you can use this command in the terminal to tell git to look in the sampledeploy
folder instead of the root folder in the repo:
git subtree push —-prefix sampledeploy heroku master
And it works! When I check in the browser, I can see the template I’ve created and I can get to the admin panel. Success, right?
Not quite.
There is one problem that’s pretty apparent when you try to sign into the admin…
😱 WHAT?
Don’t worry, this is scary looking but very fixable. We’ve moved our code over to Heroku, but Heroku thinks our database should be accessed via local host, which is clearly not right. When we sign into the admin, Heroku tries to access the database and the error shows up.
This brings us to…
Step 7: Set up your database on Heroku
In Heroku-speak, this is called ‘provisioning an add-on’ for Postgres, which you can read more about in the docs, where you can also find details about installing Postgres, using multiple databases in your app, and specifying an alias for your DB. Here we’ll keep it simple.
Python comes with Postgres support, which you can activate like so: heroku addons | grep -i POSTGRES
And your output should look something like this: heroku-postgresql (postgresql-tapered-53266) hobby-dev free created
If you want to see the URL for the database you’ve created, type:
heroku config -s | grep DATABASE_URL
And you’ll get output like this:
DATABASE_URL=’postgres://{lots of characters }.compute-1.amazonaws.com:5432/{more characters}’
Heroku handily adds DATABASE URL
as an environment variable, but how do you tell Django in the settings.py
file to look for this database? I like to use a utility called dj-database-url.
It’s in the requirements.txt
file with the sample app, but normally you’d have to install it: pip install dj-database-url
.
We’ll have to add a few lines to our settings.py
:
import dj-database-url # add this to the top of your file
db_from_env = dj_database_url.config()
DATABASES[‘default’].update(db_from_env)
If you want to add a parameter to to set the maximum age of the database connection to 600 seconds (10 minutes, for example), you would write db_from_env
as:
db_from_env = dj_database_url.config(conn_max_age=600)
If you leave it blank, Django will make a new connection with every request.
Will it work now? NO.
Remember, if you followed the previous post, we’ve migrated the database locally, but we haven’t migrated the Heroku database yet.
In the terminal, we’ll do just that — you can run the same commands as you would with your local database, but prepending heroku run
, like this:
heroku run python manage.py makemigrations
heroku run python manage.py migrate
And you should see output like this:
Now you can also create a superuser in your Heroku database to access the admin:
heroku run python manage.py createsuperuser
Head over to the admin panel (yoursubdomain.herokuapp.com/admin), plug in your new superuser credentials and behold!
Way to go — most of the hard stuff is done! But we have one more small hurdle…handling static files.
Step 8: Whitenoise and static file set up
Whitenoise is a Python package that enables your Django app to serve its own static files (like images), so you don’t need to worry about setting them up on Amazon S3 or anything like that. It can be slower to do it this way, but it is really convenient. If you prefer to serve your static files from another source, you can skip this section.
The documentation for Whitenoise is really good, and I recommend looking at it if you want to learn more, but let’s do the most basic implementation.
Whitenoise is in the requirements.txt
in the sample app, but if you aren’t starting with that, type pip install whitenoise
in the terminal.
To illustrate how the paths work, I’ve added a static
folder in our sampleapp
file, and uploaded an image at static/img/field.jpg
to test in the template.
In the settings.py
file, add the following:
- Add this to the
MIDDLEWARE
list after the security middleware:whitenoise.middleware.WhiteNoiseMiddleware,
. - Add this line wherever you like (I usually put it with my other
STATIC
variables):STATICFILES_STORAGE = ‘whitenoise.storage.CompressedManifestStaticFilesStorage’
. - If you want to run Whitenoise in development (which I recommend just to make sure you don’t see different behavior in production), add this before the
django.contrib.staticfiles
item in yourINSTALLED_APPS
list:’whitenoise.runserver_nostatic'
. You can also run your local server by typingpython manage.py runserver --nostatic
, but you need to do that every time — adding that line is less error prone.
Final step here is to source your image in the template (home.html
in the sample app).
- First, at the top of the file, tell Django to load the static files:
{% load static %}
- Then, add the path to the static files to your src — here’s what it looks like in the sample app (the
alt
is optional):<img src=”{% static “img/field.jpg” alt=”image of a field at dusk” %}”/>
And that’s it! You can test this locally now and try pushing to Heroku again to verify it works.
One more little thing and we’re done!
Step 9: Change DEBUG
to False
Quick disclaimer here — ideally, you do this before that first push to Heroku and use the heroku logs
command in the terminal to debug. It’s last here to remind you to double check.
This is important because it tells Django that you don’t want it to show detailed error messages in production. These are super helpful for debugging in development (which is why it’s set to TRUE
as the default) but these errors reveal a bunch of environment variables you don’t want out the open internet for security reasons, and are extra confusing and broken-looking for an end user. When you change DEBUG
to False
, Django serves a plain, generic error page
Just go to settings.py
and change DEBUG
to False
, commit, push to Heroku, and you’re all set.
I hope you found this blog post helpful when deploying your project! If you have any suggestions for improving this post, I’d love to hear feedback in the comments.