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:

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 called wsgi in the folder called sampledeploy — 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 the Procfile are in your project root, the same directory as manage.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 the ALLOWED_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 to ALLOWED_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.

An app is born

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.

Something worked!

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:

  1. You will lose all of your git history
  2. 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

Our template in the browser

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

Sweet success!

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 your INSTALLED_APPS list: ’whitenoise.runserver_nostatic' . You can also run your local server by typing python 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” %}”/>
Nice!

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.

Thanks for reading, and happy coding! 👩🏽‍💻