Hands-on Assignment: Deploying to Production Part 2 (Heroku & Git)
May 17, 2018
Now comes the exciting part! We’re going to learn how to deploy our application onto Heroku and have it be accessible by anyone, anywhere in the world. We’ll walk through all the installation instructions required for you to be able to easily run your web application on Heroku and easily push updates.
What is Heroku?
Heroku is a cloud platform as a service (PaaS) that enables developers to build, run, and operate applications entirely in the cloud.
Heroku is easy to set up and has a free account so that you can follow along and deploy your Reddit (or any other) application. First, we will need to set up Git, a versioning control system, for our codebase. This will allow us to easily integrate with Heroku.
Let’s dive right into it!
Step 1: Installing Git
These Git instructions are adapted from DjangoGirls and licensed under CC BY-SA 4.0.
Git is a version control system (VCS) that tracks changes to files over time. It maintains a history of every version of your code, allowing you to revisit specific versions whenever you’d like. A bit like the “track changes” feature in Microsoft Word, but much more powerful.
Heroku uses git for its deployments, so let’s go ahead and install it.
Windows
Download Git from here. Choose Windows and select the 32 bit or 64 bit option. If you’re unsure if you should use the 64-bit or 32-bit installer, go here to find out which version of Windows you have.
You can select next on all steps except for the one title Adjusting your PATH environment. On that step, select the bottom option of Use Git and optional Unix tools from the Windows Command Prompt.
Everything else can remain as the default setting.
Don’t forget to restart the command prompt or powershell after the installation has finished successfully.
OS X
Download Git from here and follow the instructions.
Note: If you are running OS X 10.6-10.8, you will need to install this version of git.
Debian or Ubuntu
$ sudo apt install git
Fedora
$ sudo dnf install git
openSUSE
$ sudo zypper install git
Step 2: Starting our Git repository and Committing
Git tracks changes to a particular set of files in what’s called a code repository (or “repo” for short). Let’s start one for our project. Open up your console and run these commands, in the proj3-starter
directory:
Note: Check your current working directory with a
pwd
(Mac OS X/Linux) orcd
(Windows) command before initializing the repository. You should be in theproj3-starter
folder.
$ git init
Initialized empty Git repository in ~/proj3-starter/.git/
$ git config --global user.name "Your Name"
$ git config --global user.email you@example.com
Initializing the git repository is something we need to do only once per project (and you won’t have to re-enter the username and email ever again).
Git will track changes to all the files and folders in this directory, but there are some files we want it to ignore. We do this by creating a file called .gitignore
in the base directory. Open up your editor and create a new file with the following contents:
*.pyc
*~
__pycache__
myvenv
db.sqlite3
/static
.DS_Store
And save it as .gitignore
in the proj3-starter
folder.
Some notes about the above file. Beginning a line with *
matches any folder or file that ends with the following string. For example, for *.pyc
, we will ignore all files that end with .pyc
. myenv
will match the myenv
folder or file at the root.
Note: The dot at the beginning of the file name is important! If you’re having any difficulty creating it (Macs don’t like you to create files that begin with a dot via the Finder, for example), then use the “Save As” feature in your editor; it’s bulletproof.
Note: One of the files we specified in our
.gitignore
file isdb.sqlite3
. Even though we’re using a Postgres database for this project, we thought it was a good idea to add this file in, in case you decide to usesqlite
for your own projects. Since the sqlite file is your local database, where all of your local data is stored, we don’t want to add this to your repository because your website on your production environment (like Heroku) is going to be using a different database. That database could be SQLite, like your development machine, but usually you will use one called MySQL or Postgres which can deal with a lot more site visitors than SQLite. Either way, by ignoring your SQLite database for the git copy, it means that all of the date that you’ve created so far (like your subreddits and posts) are going to stay and only be available locally, but you’re going to have to add them again on production. You should think of your local database as a good playground where you can test different things and not be afraid that you’re going to delete your real data when you push to production.
It’s a good idea to use a git status
command before git add
or whenever you find yourself unsure of what has changed. This will help prevent any surprises from happening, such as wrong files being added or committed. The git status
command returns information about any untracked/modified/staged files, the branch status, and much more.
The output should be similar to the following:
$ git status
On branch master
Initial commit
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
manage.py
mysite/
reddit/
requirements.txt
nothing added to commit but untracked files present (use "git add" to track)
Let’s commit all our changes thus far. Make sure you’re at the root proj-3-starter
directory. Go to your console and run these commands:
$ git add --all .
$ git commit -m "My Reddit Production App, first commit"
[...]
31 files changed, 928 insertions(+)
[...]
git add -all .
tells git to stage all the changed files in our working directory. git commit
stores these current changes in a new commit with a message describing the change (denoted by -m
).
Step 3: Create a Heroku Account
First, we need to create a free Heroku account. Go to their Sign up page and follow the instructions to create your account. Remember your email and password as you’ll need it to set up the CLI next.
Step 4: Install Heroku Command Line Interface (CLI)
Install Heroku CLI from here. Use the instructions from earlier to see if you should use the 64-bit or 32-bit installer.
Windows
Click on the Download the installer under Windows
Mac
Click on the Download the installer under macOS.
Ubuntu / Debian
Run the following from your terminal:
$ curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
Step 5: Update requirements.txt
Let’s go back to our requirements.txt
file and add the following packages.
dj-database-url==0.5.0
gunicorn==19.8.1
whitenoise==3.3.1
Your requirements.txt
file should now look like:
django==2.0.6
psycopg2==2.7.4
dj-database-url==0.5.0
gunicorn==19.8.1
whitenoise==3.3.1
If you’re using Django 1.11, the first line above will be
django==1.11.0
These packages will help us run our Django application on Heroku. We’ll briefly go over what each package does.
- dj-database-url — a simple Django utility that will help us generate the proper connection string for our Postgres database on Heroku.
- gunicorn — a powerful web server that is often used in production for Django and other Python web applications.
- whitenoise — Django doesn’t support serving static files out of the box in production.
Whitenoise
helps us serve static assets right from Gunicorn. Note that when you do create a production deployment where you expect a lot of users, you’d want to serve your static files through a Content Delivery Network (CDN). CDN’s help speed up delivery of these assets since they are specialized servers distributed all around the world. We won’t cover that in this walkthrough though.
Lets install all these packages. Make sure you’re inside your virtual environment.
pip install -r requirements.txt
Step 6: Restructure settings.py
A good Django practice is to separate out the settings for your development and production environment, while maintaining all the common settings in one file. In the end we will want something that looks like the following:
We’ve replaced settings.py
inside mysite
with a settings
folder. It has three files inside of it: common.py
, dev.py
, and prod.py
. Go ahead and create the settings
folder and the three files inside of it.
Django 2.0
common.py
will contain all of the common settings that don’t change between development and production. For our app that will be things such as INSTALLED_APPS
, MIDDLEWARE
, etc. (It is possible to have different settings for these as well between dev and prod but that’s not the case for our app). Paste the following into your common.py
.
"""
Django settings for mysite project.
Generated by 'django-admin startproject' using Django 1.9.7.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '^p41e*79z0_=0^2^i7@%^4z_&ray$6zn0*o4wsly503*%m()cm'
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'reddit'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'mysite.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'mysite.wsgi.application'
# Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
LOGIN_REDIRECT_URL = '/'
Django 1.11
common.py
will contain all of the common settings that don’t change between development and production. For our app that will be things such as INSTALLED_APPS
, MIDDLEWARE_CLASSES
, etc. (It is possible to have different settings for these as well between dev and prod but that’s not the case for our app). Paste the following into your common.py
.
"""
Django settings for mysite project.
Generated by 'django-admin startproject' using Django 1.9.7.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '^p41e*79z0_=0^2^i7@%^4z_&ray$6zn0*o4wsly503*%m()cm'
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'reddit'
]
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'mysite.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'mysite.wsgi.application'
# Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
LOGIN_REDIRECT_URL = '/'
Alright, back to the common instructions, regardless of Django version!
On line 38, we added the whitenoise.middleware.WhiteNoiseMiddleware
middleware. As we explained above, WhiteNoise
allows our webapp to serve static files in production.
A middleware is a framework that hooks into Django’s request/response processing. For every request or response that goes through your Django application, your middlewares will be invoked. Each middleware component is responsible for doing some specific function. For example, Django includes a middleware component, AuthenticationMiddleware, that associates users with requests using sessions.
dev.py
dev.py
will contain just the development settings. Paste the following into your dev.py
file.
from .common import *
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'cl',
'USER': 'name',
'PASSWORD': '',
'HOST': 'localhost',
'PORT': '5432',
}
}
A few important points to note:
- We import all of the
common.py
settings in the first line withfrom .common import *
DEBUG
isTrue
since this file is just for development- Our
DATABASE
settings are our local database settings that we had in Part 1. Remember to replacename
next toUSER
with your own user name that you created earlier.
prod.py
prod.py
will contain just the production settings. Paste the following into your prod.py
file.
from .common import *
import dj_database_url
DEBUG = False
ALLOWED_HOSTS = ['.herokuapp.com']
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'cl',
'USER': 'name',
'PASSWORD': '',
'HOST': 'localhost',
'PORT': '5432',
}
}
db_from_env = dj_database_url.config(conn_max_age=500)
DATABASES['default'].update(db_from_env)
A few important points to note:
DEBUG
isFalse
since this file is just for production.- Our
DATABASE
settings will now be our production settings. Remember to replacename
next toUSER
with your own user name that you created earlier. - On lines 22, we use the
dj_database_url
package to generate our database connection information for Heroku. On line 23, we update ourDATABASES
dictionary with the updated information. - We’ve updated our
ALLOWED_HOSTS
variable to point to only['.herokuapp.com]
.
Make it a package
You’ll need to create a new file inside your settings
folder called __init__.py
. This let’s Python know to treat settings
as a package so that you can refer to the dev/prod files as mysite.settings.dev
and mysite.settings.prod
. You can leave this as an empty file.
Step 7: Web Server Gateway Interface (WSGI)
WSGI is the Python standard for deploying web applications. When you run the startproject
command, Django automatically creates a default WSGI configuration for you. Take a look at mysite/wsgi.py
— it should look like the following:
"""
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.9/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = get_wsgi_application()
Look at line 14. It settings the DJANGO_SETTINGS_MODULE
to mysite.settings
. This makes a lot of sense when we just had a settings.py
file inside of the mysite
folder, but now we’ve made settings
a directory. Let’s go ahead and update this line to:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings.prod")
We’re explicitly referring to the production version of the settings file here.
Step 8: Updating manage.py
Take a look at manage.py
located at the root directory (proj3-starter/manage.py
). It looks like:
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
Now we’ll also go ahead and update line 5 to use mysite.settings.prod
.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings.prod")
Development settings
You may be wondering how you’ll use manage.py
in development if we’re updating DJANGO_SETTINGS_MODULE
to only use the production settings file. The trick is that whenever we run a manage.py
command in development, we will pass in the --settings
flag. For example:
python manage.py runserver --settings=mysite.settings.dev
python manage.py migrate --settings=mysite.settings.dev
Important: This is worth repeating! Every time you use manage.py
in development, always pass in the --settings
flag.
Now whenever Heroku runs any of the manage.py
commands, it will use the prod
settings file (via the DJANGO_SETTINGS_MODULE
environment variable), but whenever we run it in development, we’ll use the dev
settings file via the --settings
flag.
Step 9: Heroku Procfile
A Procfile
is a text file in the root directory of our application that lets Heroku know what command to run to start our application. Lets create the file (called Procfile
) in our root proj3-starter
folder.
Add the following line and save it:
web: gunicorn mysite.wsgi --log-file -
Let’s unpack this command. It declares a single process type, web
, that will be run via the command gunicorn mysite.wsgi
. The --log-file -
option just tells the gunicorn
command to write the logs to the console.
The name web
is critical here — it lets Heroku know that this process should receive web traffic and be attached to it’s HTTP (web server) routing stack.
Step 10: Creating the runtime.txt
File
We need to let Heroku know which version of Python we’ll be using. To do this, we’ll create a runtime.txt
file at the root directory (same place we created the Procfile
).
Simply paste in the following and save it:
python-3.6.4
Step 11: Deploying to Heroku
Hang on tight, we’re almost done!!
Let’s finish setting up Heroku and deploying to it.
Authenticate
First, we need to authenticate with Heroku. Run the following command:
$ heroku login
Log in using the email address and password you used when creating your Heroku account.
Pick a name for our Application
When you deploy your application on Heroku, it provides you a url such as <your app name>.herokuapp.com
. This app name needs to be unique across all the application on Heroku. We will call ours cl-reddit-demo
but you should feel free to name your’s whatever you’d like. Remember that you can only use lowercase letters, numbers, and dashes in your application name.
$ heroku create cl-reddit-demo
If you don’t want to pick a name for yourself, you can let Heroku pick one for you by running:
$ heroku create
You can always rename your application by running:
$ heroku apps:rename your-new-app-name-here
When you run the create
command above, Heroku automatically adds itself as the remote
repository. A remote
repository is a version of our code that is hosted somewhere else on the internet or network — in our case it’s hosted on Heroku.
Pushing to Heroku
When we push
our latest code to Heroku, we are updating the version of our code that lives on Heroku with our latest commit.
Remember to always commit your latest code locally before pushing to Heroku or else you’ll have an outdated version on Heroku. Let’s make sure all our changes are committed.
$ git add --all .
$ git commit -m "Your commit message here!"
You’ll know that your commit was successful if when you run git status
it shows nothing to commit, working tree clean
.
$ git status
On branch master
nothing to commit, working tree clean
Next we’ll push our git repository to Heroku. Run the following command:
$ git push heroku master
This might take a bit of time since Heroku has to install python, all of the packages in our requirements.txt
file, etc.
Start the Web Process
Remember that we specified the web
process in our Procfile
. We now need to let Heroku know that it should start this process.
Run the following command:
$ heroku ps:scale web=1
This command tells Heroku to run 1
instance of our web
process. These instances are known as Dynos
in Heroku land.
We have a pretty simple application so we don’t need more than one Dyno, but you can imagine a popular website like Reddit having tens of thousands of Dynos serving hundreds of millions of people.
Note that if you try running more than 1 Dyno, you’ll have to start paying Heroku.
Setting up our Database and Superuser
If you were to try opening your web application right now, you’d run into a bunch of errors since we haven’t ran any migrations or created our superuser. Let’s do that now:
$ heroku run python manage.py migrate
$ heroku run python manage.py createsuperuser
When creating your superuser, remember to create a username/password that you’ll remember as you’ll need it when logging into the Django admin console and your application.
Visit your Application
You can visit your app in your browser by typing:
$ heroku open
You’ll notice that the url should be https://<your-app-name>.herokuapp.com/
Conclusion
Congratulations :). You’ve deployed your first application onto Heroku. Feel free to make any changes you like to the application. You can keep committing the changes and pushing to Heroku as many times as you like!
You can use this process to deploy any of your sample projects and share the website with your friends.