This guide provides instructions on deploying a Django app to Heroku, DataMade's preferred platform for hosting dynamic applications.
The easiest way to properly set up your application code for Heroku is to use our Django template to create a fresh Heroku-enabled Django app. The template includes additional nice features like ES6 support and GitHub Actions configuration, but it is only appropriate for brand new apps. Once you've created your app, you can skip to learning how to Provision Heroku resources.
If you'd like to convert an existing app to Heroku, or if the template is unfeasible for some other reason, see Set up application code for Heroku.
- Provision Heroku resources
- Set up Slack notifications
- Enable additional services
- Set up a custom domain
- Set up application code for Heroku
- Troubleshooting
If you initialized your application with DataMade's new-django-app
Cookiecutter template, your app is already configured to run on Heroku.
If you are migrating an existing app, or if you want to learn more about the configuration that comes with the template, see Set up application code for Heroku, below, before proceeding with this step.
The following instructions will help you deploy your properly configured application to the platform.
The fastest way to get a project up and running on Heroku is to use the Heroku CLI. Before you start, make sure you have the CLI installed locally. Once you install the CLI, you'll need to switch over to the CLI's beta version. This allows you to use the manifest
CLI plugin, which you must install:
heroku update beta
Confirm that you have the manifest plugin installed:
heroku manifest --help
In order to deploy your project, you need to create Heroku apps for staging
and production, and tie them together in a pipeline. Be sure that your application is committed to version control before you begin, and that the repo has a heroku.yml
file. Otherwise Heroku won't use the correct buildpack — we use a container buildpack and not one of their default Python buildpacks.
To create these resources, start by defining the name of your app:
# This should be the same slug as your project's GitHub repo.
export APP_NAME=<your-app-name-here>
Then, run the following Heroku CLI commands to create a staging app and a pipeline:
heroku create ${APP_NAME}-staging -t datamade --manifest
heroku pipelines:create -t datamade ${APP_NAME} -a ${APP_NAME}-staging -s staging
heroku pipelines:connect ${APP_NAME} -r datamade/${APP_NAME}
Your CLI output should look like this:
heroku create ${APP_NAME}-staging -t datamade --manifest
Reading heroku.yml manifest... done
Creating ⬢ demo-app-staging... done, stack is container
Adding heroku-postgresql... done
https://demo-app-staging.herokuapp.com/ | https://git.heroku.com/demo-app-staging.git
heroku pipelines:add ${APP_NAME}-staging -a ${APP_NAME}-staging -s staging
Adding ⬢ demo-app-staging to datamade-app pipeline as staging... done
If you would like to set up a production app as well, run the following commands to create one and add it to your pipeline:
heroku create ${APP_NAME} -t datamade --manifest
heroku pipelines:add ${APP_NAME} -a ${APP_NAME} -s production
Once you have the environments you need, enable review apps for your pipeline:
# Note that these need to be two separate commands due to an open Heroku bug,
# since --autodeploy and --autodestroy require a PATCH request
heroku reviewapps:enable -p ${APP_NAME}
heroku reviewapps:enable -p ${APP_NAME} --autodeploy --autodestroy
Next, configure environment variables for staging and production apps. DJANGO_SECRET_KEY
should be a string generated using the XKCD password generator,
and DJANGO_ALLOWED_HOSTS
should be a comma-separated string of valid hosts
for your app.
heroku config:set -a ${APP_NAME}-staging DJANGO_SECRET_KEY=<random-string-here>
heroku config:set -a ${APP_NAME}-staging DJANGO_ALLOWED_HOSTS=.herokuapp.com
# Run these commands if you have a production application
heroku config:set -a ${APP_NAME} DJANGO_SECRET_KEY=<random-string-here>
heroku config:set -a ${APP_NAME} DJANGO_ALLOWED_HOSTS=<your-list-of-allowed-hosts-here>
Note that review app config vars cannot yet be set using the CLI, but you can set them in
the Heroku dashboard by navigating to the pipeline home page and visiting
Settings > Review Apps > Review app config vars
in the nav.
You can also set them in the app.json
file, but only set non-sensitive values since that file is committed to version control. See this code for an example, and the Heroku docs for more information about the app.json
schema.
Also note that while DATABASE_URL
is probably required by your application, you don't actually
need to set it yourself. The Heroku Postgres add-on will automatically define this
variable
when it provisions a database for your application.
Heroku can deploy commits to specific branches to different environments (e.g. staging vs. production).
Follow the Heroku documentation
to enable automatic deploys from main
to your staging app. Be sure to check
Wait for CI to pass before deploy
to prevent broken code from being deployed!
For production deployments, create a long-lived deploy
branch off of main
and
configure automatic deployments from deploy
to production.
# create deploy branch (first deployment)
git checkout main
git pull origin main
git checkout -b deploy
git push origin deploy
# sync deploy branch with main and deploy to production (subsequent deployments)
git checkout main
git pull origin main
git push origin main:deploy
Creating your production instance from our template Heroku artifacts will provision hobby-grade resources for your application. Ahead of launch, plan time to upgrade your dyno and database to at least the first production-grade tier. That's Standard-1x for dynos and Standard-0 for Postgres.
Consult the Heroku documentation on:
Heroku can send build notifications to Slack via the Heroku ChatOps integration. This integration should already be set up in our Slack channel, but if you need to install it again, see the official documentation.
To enable notifications for an app, run the following Slack command in the corresponding channel:
/h route ${PIPELINE_NAME} to ${CHANNEL_NAME}
For example, to enable notifications for the parserator
pipeline in the #parserator
channel, we would run /h route parserator to #parserator
.
If your app requires additional services, like Solr or PostGIS, you'll need to perform some extra steps to set them up for your pipeline.
In the absence of an affordable Solr add-on, we deploy apps that use Solr using our legacy AWS deployment pattern. Consult senior staff.
If your app requires PostGIS, you'll need to manually enable it in your database. Once your database has been provisioned, run the following command to connect to your database and enable PostGIS:
heroku psql -a <YOUR_APP> -c "CREATE EXTENSION postgis"
To automate this process, you can include a step like this in scripts/release.sh
to make sure PostGIS is always enabled in your databases:
psql ${DATABASE_URL} -c "CREATE EXTENSION IF NOT EXISTS postgis"
All Heroku apps are automatically delegated a subdomain under the heroku.com
root domain, like example.heroku.com
. This automatic Heroku subdomain
is usually fine for review apps and staging apps, but production apps almost
always require a dedicated custom domain like example.com
.
When you're ready to deploy to production and publish your app publicly, you'll need to set up a custom domain. In order to do this, you need to register the custom domain in two places: in the Heroku dashboard, and in your (or your client's) DNS provider. Then, you'll need to instruct Heroku to enable SSL for your domain.
For detailed documentation on setting up custom domains, see the Heroku docs.
The first step to setting up a custom domain is to instruct Heroku to use the
domain for your app. Navigate to Settings > Domains
in your app dashboard, choose
Add domain
, and enter the name of the custom domain you would like to use.
When you save the domain, Heroku should display the DNS target for your domain. Copy this string and use it in the next step to delegate the domain with your DNS provider.
Note: If you're not comfortable with basic DNS terminology and you're finding this section to be confusing, refer to the CloudFlare docs on how DNS works.
Once you have a DNS target for Heroku, you need to instruct your DNS provider to direct traffic for your custom domain to Heroku.
If you're setting up a custom subdomain, like www.example.com
or app.example.com
,
you'll need to create a CNAME
record pointing to your DNS target with your DNS
provider. For more details, see the Heroku docs on configuring DNS for
subdomains.
If you're setting up a custom root domain, like example.com
, you'll need
to create the equivalent of an ALIAS
record with your DNS provider. Not all
DNS providers offer the same type of ALIAS
record, so to provision this record
you should visit the Heroku docs on configuring DNS for root
domains
and follow the instruction for your provider. At DataMade we typically use Namecheap,
which allows you to create ALIAS
records.
After creating the appropriate DNS record with your DNS provider, wait a few
minutes for DNS to propagate and confirm that you can load your app by visiting
your custom domain. Remember that Django will only serve domains that are listed
in its ALLOWED_HOSTS
settings variable, so you may have to update your DJANGO_ALLOWED_HOSTS
config var on Heroku to accomodate your custom domain.
Once your custom domain is properly resolving to your app, navigate to
Settings > SSL Certificates
in your app dashboard, select Configure SSL
,
and Choose Automatic Certificate Management (ACM)
. Your app should now load
properly when you visit it with the https://
protocol.
As a final step, we want to make sure that the app always redirects HTTP traffic
to HTTPS. Heroku can't do this for us,
so we need to configure the app code to do it. If you didn't use the Django template
to create your app, add the following settings to your settings.py
file:
if DEBUG is False:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
When you deploy this change and try to load your app with the http://
protocol,
it should now automatically redirect you to https://
and display a valid certificate.
For most of our Heroku sites, we go with Automatic Certificate Management (ACM) for our SSL certifications. However, for domains that already have an SSL certification for the entire domain like *.example.com
, the SSL certificate must be set up manually:
Heroku provides documentation on how to create the necessary CSR file that SSL providers require: https://devcenter.heroku.com/articles/acquiring-an-ssl-certificate
When setting up custom domains, follow these general guidelines:
- Where possible, let the client register the domain name so we don't have to manage it.
- Shorter domains are always better, but we usually defer to our clients' preferences when choosing a custom domain name.
- If the client has a pre-existing root domain, always advise deploying on a subdomain
like
app.example.com
instead of a path likeexample.com/app
. Clients often ask for paths off of root domains, but they are typically quite hard to deploy. - If using a root domain, make sure to set up a
www
subdomain to redirect to the root. - Don't allow
.herokuapp.com
inDJANGO_ALLOWED_HOSTS
in production, since we want the custom domain to be canonical for search engine optimization.
In order to deploy a legacy Django application to Heroku, a few specific configurations need to be enabled in your application code.
We use Heroku as a platform for deploying containerized apps, which means that your app must be containerized in order to use Heroku properly. If your app is not yet containerized, follow our instructions for containerizing Django apps before moving on.
There are two commands you should make sure to add to your Dockerfile in order to properly deploy with Heroku:
- In the section of your Dockerfile where you install OS-level dependencies with
apt-get
, make sure to installcurl
so that Heroku can stream logs during releases (if you're inheriting from the officialpython
images,curl
will already be installed by default) - Toward the end of your Dockerfile, run
python manage.py collecstatic --noinput
so that static files will be baked into your container on deployment
For an example of a Django Dockerfile that is properly set up for Heroku, see the Minnesota Election Archive project.
For new projects, you can skip this step. For existing projects that are being convered from our older deployment practices, you'll want to consolodate everything into settings.py
and eventually delete your settings_local.py
and supporting files. In switching to Heroku, the settings_local.py
pattern is no longer necessary to store secret values as we'll be using environment variables instead.
In addition, you will want to delete the following files, as we won't be using Travis, Blackbox or CodeDeploy:
.travis.yml
(we will be using GitHub Actions instead of Travis for CI)appspec.yml
APP_NAME/settings_local.example.py
(secret values are now stored as environment variables)configs/nginx.xxx.conf.gpg
filesconfigs/supervisor.xxx.conf.gpg
filesconfigs/settings_local.xxx.py.gpg
filesconfigs/settings_local.travis.py
fileskeyrings/live/pubring.kbx
(Blackbox is no longer needed as we're using environment variables)scripts/after_install.sh.gpg
(no longer using AWS CodeDeploy)scripts/before_install.sh.gpg
scripts/app_start.sh.gpg
scripts/app_stop.sh.gpg
For an example of a conversion, see this PR for the Erikson EDI project (private repo)
Apps deployed on Heroku don't typically use Nginx to serve content, so they need some other way of serving static files. Since our apps tend to have relatively low traffic, we prefer configuring WhiteNoise to allow Django to serve static files in production.
Follow the setup instructions for WhiteNoise in
Django to ensure that your
Django apps can serve static files. You will also need to include whitenoise
in your requirements.txt
as a dependency.
In order to be able to serve static files when DEBUG
is False
, you'll also want
to make sure that RUN python manage.py collectstatic
is included as a step in your
Dockerfile. This will ensure that the static files are baked into the container.
Since we typically mount application code into the container during local development,
you'll also need to make sure that your static files are stored outside of the root
project folder so that the mounted files don't overwrite them. One easy way to do this
is to set STATIC_ROOT = '/static'
in your settings.py
file.
Heroku doesn't allow us to decrypt content with GPG, so we can't use Blackbox to decrypt application secrets. Instead, we can store these secrets as config vars, which Heroku will thread into our container environment.
The three most basic config vars that you'll want to set for every app include
the Django DEBUG
, SECRET_KEY
, and ALLOWED_HOSTS
variables. Update settings.py
to read these variables from the environment:
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = False if os.getenv('DJANGO_DEBUG', True) == 'False' else True
allowed_hosts = os.getenv('DJANGO_ALLOWED_HOSTS', [])
ALLOWED_HOSTS = allowed_hosts.split(',') if allowed_hosts else []
Make sure to update your app service in docker-compose.yml
to thread any variables
that don't have defaults into your local environment:
services:
app:
environment:
- DJANGO_SECRET_KEY=really-super-secret
For a full example of this pattern in a production app, see the Docker Compose file and Django settings file in the UofM Election Archive project.
When Gunicorn is running our app on Heroku, we generally want it to log to stdout
and stderr instead of logging to files, so that we can let Heroku capture the logs
and see view them with the heroku logs
CLI command (or in the web console).
By default, Django will not log errors to the console when DEBUG
is False
(as documented here).
To make sure that errors get logged appropriately in Heroku, set the following
baseline logging settings in your settings.py
file:
import os
LOGGING = {
'version': 1,
'disable_existing_loggers': False, # Preserve default loggers
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
},
}
For more detail on Django's logging framework, see the documentation.
If you're converting an existing app to use Heroku, create the following config files relative to the root of your repo. If you're setting up a Heroku deployment for an app that you created with the Django template, you should have these config files already, and you can safely skip to Create apps and pipelines for your project.
The heroku.yml
manifest file tells Heroku how to build and network the services
and containers that comprise your application. This file should live in the root of your project repo.
For background and syntax, see the
documentation.
Use the following baseline to get started:
# Define addons that you need for your project, such as Postgres, Redis, or Solr.
setup:
addons:
- plan: heroku-postgresql
# Define your application's Docker containers.
build:
docker:
web: Dockerfile
# Define any scripts that you'd like to run every time the app deploys.
release:
command:
- ./scripts/release.sh
image: web
# The command that runs your application. Replace 'app' with the name of your app.
run:
web: gunicorn -t 180 --log-level debug app.wsgi:application
In your app's scripts
folder, define a script release.sh
to run every time the app deploys.
Use the following baseline script and make sure to run chmod u+x scripts/release.sh
to make it executable:
#!/bin/bash
set -euo pipefail
python manage.py collectstatic --noinput
python manage.py migrate --noinput
python manage.py createcachetable && python manage.py clear_cache
If your app uses a dbload
script to load initial data into the database, you can use release.sh
to check if the initial data exists and run the data loading scripts if not. For example:
# Set ${TABLE} to the name of a table that you expect to have data in it.
if [ `psql ${DATABASE_URL} -tAX -c "SELECT COUNT(*) FROM ${TABLE}"` -eq "0" ]; then
make all
fi
The release.sh
file must be set to executable at the file system level. To do this, run chmod +x scripts/release.sh
on your local machine and commit the change. Surprisingly, GitHub will recognize this change!
Note that logs for the release phase won't be viewable in the Heroku console unless curl
is installed in your application container. Make sure your Dockerfile installs
curl
to see logs as scripts/release.sh
runs.
In order to enable review apps
for your project, the repo must contain an app.json
config file in the root directory.
For background and syntax, see the documentation.
Use the following baseline to get started:
{
"name": "your-app",
"scripts": {},
"env": {
"DJANGO_SECRET_KEY": {
"required": true
},
"DJANGO_ALLOWED_HOSTS": {
"required": true
}
},
"formation": {
"web": {
"quantity": 1,
"size": "hobby"
}
},
"environments": {
"review": {
"addons": ["heroku-postgresql:hobby-basic"]
}
},
"buildpacks": [],
"stack": "container"
}
For Heroku deployments, we use GitHub Actions for CI (continuous integration). Read the how-to to set up GitHub Actions.
If your app isn't loading at all, check the dashboard to make sure that the most
recent build and release cycle passed. If the build and release both passed,
check the Dyno formation
widget on the app Overview
page to make sure that
dynos are enabled for your web
process.
Sometimes a Heroku CLI command will fail without showing much output (e.g. Build failed
).
In these cases, you can set the following debug flag to show the raw HTTP responses
from the Heroku API:
export HEROKU_DEBUG=1
Heroku can't stream release logs unless curl
is installed in the application
container. Double-check to make sure your Dockerfile installs curl
.
If curl
is installed and you still see no release logs, try viewing all of your app's logs by
running heroku logs -a <YOUR_APP>
. Typically this stream represents the most
complete archive of logs for an app.
The Heroku CLI provides a command, heroku psql
, that you can use to connect
to your database in a psql
shell. See the docs for using this
command.
You can use the Heroku CLI to accomplish this task. See the Heroku docs on sharing databases between applications.
If a review app requires loading in data with more than 10,000 rows, Heroku will send an angry email to whoever "deployed" that review app saying that disruption of the database is imminent because of exceeded row limits.
If the email is indeed referring to a review app, you can safely ignore it because "database disruption"
means that INSERT
operations will be revoked in seven days and for most review
apps this is beyond the amount of time the app will be active anyway. If the email is
instead referring to a production app or a long-lived staging app, you should
upgrade your Heroku Postgres plan
for that instance to insure that database function continues.
In an ideal world it would be nice to configure apps that require >10,000 rows of data to use a larger Heroku Postgres plan for review apps. Unfortunately, there is not currently a way to set this type of configuration (and hence prevent these kinds of emails being sent for review apps) because Heroku defaults to the cheapest plan for review app addons.
You might deploy a review app and everything works. Then you merge your code to main
, which builds a new version to the staging pipeline. But for some reason, there is no database provisioned for staging.
Did you have the manifest
CLI plugin installed when you first created the Heroku pipeline? If not, then it won't provision the Postgres add-on. See this step.
Here's an example where the manifest
plugin was not installed when creating an app:
heroku create ${APP_NAME}-staging -t datamade --manifest
Creating ⬢ demo-app-staging... done
https://demo-app-staging.herokuapp.com/ | https://git.heroku.com/demo-app-staging.git
Here is an example where everything worked because the manifest
plugin was installed:
heroku create ${APP_NAME}-staging -t datamade --manifest
Reading heroku.yml manifest... done
Creating ⬢ demo-app-staging... done, stack is container
Adding heroku-postgresql... done
https://demo-app-staging.herokuapp.com/ | https://git.heroku.com/demo-app-staging.git
heroku pipelines:add ${APP_NAME}-staging -a ${APP_NAME}-staging -s staging
Adding ⬢ demo-app-staging to datamade-app pipeline as staging... done
The difference is in the CLI's output. In the working example, note the output Reading heroku.yml manifest... done
and Adding heroku-postgresql... done
.
If that is not the problem, then make sure your app's heroku.yml
is configured correctly. When Heroku builds your app to a pipeline, it uses the heroku.yml
file to provision the resources (like Postgres or Solr).