Deploying Django Web App Using Heroku (Updated)

Heroku is one of the most popular hosting platforms for web applications. In this article, I will provide a step-by-step guide on how to deploy a Django web application in Heroku.

  1. Signup for Heroku if you don't have an existing account.

  2. Install the Heroku CLI. For MacOS, use $ brew install heroku.

  3. Log in to your Heroku account by entering your credentials using $ heroku login or $ heroku login -i if you faced IP address mismatch issue like the one shown below: image.png

    $ heroku login
    Enter your Heroku credentials:
    Email: [email protected]
    Password: *********
    Logged in as [email protected]
    
  4. Create a new Heroku app either via Heroku CLI ($ heroku create APP_NAME) or directly in the Heroku dashboard: alt text

  5. Add the Heroku remote via $ heroku git:remote -a your-heroku-app alt text

  6. Configure the Heroku buildpacks. For Django, we just need 1 buildpack for Python: $ heroku buildpacks:set heroku/python. Run $ heroku buildpacks to see the configured buildpack. For your information, if you have multiple buildpacks, the last buildpack on the list determines the process type of the app. alt text

  7. Configure PostgreSQL Heroku addon

* During production, Heroku will **not be using SQLite database**. Instead, we need to use **PostgreSQL** by configuring the addon to our app using `$ heroku addons:create heroku-postgresql:hobby-dev`
* You can check whether this is successful by running `$ heroku config`:
```Shell
    $ === APP_NAME Config Vars
    DATABASE_URL: postgres://[DATABASE_INFO_HERE]
```
* The database info from the code snippet above refers to the URL containing your database’s location and access credentials all in one. Anyone with this URL can access your database, so be careful with it.
* You will notice that Heroku saves it as an **environment variable** called `DATABASE_URL`. This URL can and does change, so you should never hard code it. Instead, we’ll use the variable `DATABASE_URL` in  Django.
  • Please note that APP_NAME mentioned here onwards refers to the Django project name, NOT the individual apps within the Django project!
  1. Configure Heroku config variables
* According to Heroku, **config variables** are environment variables that can change the way your app behaves. In addition to creating your own, some add-ons come with their own.
* There are several environment variables that need to be set:
```Shell
    $ heroku config:set ALLOWED_HOSTS=APP_NAME.herokuapp.com
    $ heroku config:set ALLOWED_HOSTS=APP_NAME.herokuapp.com
    $ heroku config:set SECRET_KEY=DJANGO_SECRET_KEY
    $ heroku config:set WEB_CONCURRENCY=1
```
  1. Install the following essential Python libraries using pip install:
$ pip install django-heroku gunicorn python-dotenv dj-database-url whitenoise psycopg2
  • django-heroku is a Django library for Heroku applications that ensures a more seamless deployment and development experience.

    • This library provides:
      • Settings configuration (Static files / WhiteNoise)
      • Logging configuration
      • Test runner (important for Heroku CI)
  • dj-database-url allows the app to automatically use the default database depending on the environment the app is in. For example, if the app is run locally, it will use SQLite3 whereas in Heroku, it will default to PostgreSQL.

  • python-dotenv is useful for setting up and load environment variables.

  • gunicorn is a Python web server that is commonly used for deployment.

  • whitenoise allows your web app to serve its own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service (especially useful on Heroku and other PaaS providers).

  • psycopg2 is the most popular PostgreSQL database adapter for the Python programming language. This is essential to allow the Django web app to use external PostgreSQL database.

  1. Create a .env file containing DATABASE_URL=sqlite:///db.sqlite3. This is to tell Django to use SQLite when running locally. We don’t want .env to make it to Heroku, because .env is the part of our app that points to SQLite and not PostgreSQL. Hence, we need git to ignore .env when pushing to Heroku.
$ echo 'DATABASE_URL=sqlite:///db.sqlite3' > .env
        $ echo '.env' >> .gitignore
  1. Configure installed libraries in settings.py
  • First, import the following packages at the top of the file:
import django_heroku
import dj_database_url
import dotenv
  • Load the environment variables:
from dotenv import load_dotenv
load_dotenv()
# or
dotenv_file = os.path.join(BASE_DIR, ".env")
if os.path.isfile(dotenv_file):
    dotenv.load_dotenv(dotenv_file)
  • Next, we configure the DEBUG, SECRET_KEY and ALLOWED_HOSTS. DEBUG must be set to False during production. ALLOWED_HOSTS should also only point to the Heroku address once deployed (which is when DEBUG is set to False).
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '' # Change to empty string
DEBUG = False
# ALLOWED_HOSTS = ['*']
if DEBUG:
    ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
else:
    ALLOWED_HOSTS = ['https://APP_NAME.herokuapp.com/']
  • Then, we configure whitenoise middleware, static and media files settings. Django security middleware should already be the first thing on the list. Never load any middleware before Django security.
MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'whitenoise.middleware.WhiteNoiseMiddleware',
        # ...
 ]
# ...
STATIC_URL = '/static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
X_FRAME_OPTIONS = 'SAMEORIGIN'
  • Afterwards, update the DATABASES using dj-database-url. The idea here is to clear the DATABASES variable and then set the 'default' key using the dj_database_url module. This module uses Heroku’s DATABASE_URL variable that we set up previously if it’s on Heroku, or it uses the DATABASE_URL we set in the .env file if we’re working locally.
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }
DATABASES = {}
DATABASES['default'] = dj_database_url.config(conn_max_age=600)
  • Lastly, configure django-heroku and SSL issue workaround at the very bottom of the file. If you ran the Django application, you might get an error when working locally because the dj_database_url module wants to log in with SSL. Heroku PostgreSQL requires SSL, but SQLite doesn’t need or expect it. So, we’ll use a hacky workaround to get dj_database_url to forget about SSL at the last second:
django_heroku.settings(locals())
options = DATABASES['default'].get('OPTIONS', {})
options.pop('sslmode', None)
  1. Set up Heroku-specific files

    A. runtime.txt

    Heroku will install a default Python version if you don't specify one, but if you want to pick your Python version, you'll need a runtime.txt file.

    Create one in the root directory, next to your requirements.txt, manage.py, .gitignore and the rest. Specify your Python version with the prefix python- that you want your application to run on. Heroku usually recommends running on the latest stable version of Python:

        python-3.9.5
    

    B. requirements.txt

    When deploying the web app, Heroku will need to install all the required dependencies for the web app to run by referring to the requirements.txt file.

    To ensure that all dependencies are included, consider freezing your dependencies using the command $ pip freeze > requirements.txt. This will make your build a little bit more predictable by locking your exact dependency versions into your Git repository. If your dependencies aren't locked, you might find yourself deploying one version of Django one day and a new one the next.

    C. Procfile

    Heroku apps include a Heroku-specific Procfile that specifies the processes our application should run. The processes specified in this file will automatically boot on deploy to Heroku.

    Create a file named Procfile in the root level directory using $ touch Procfile command, right next to your requirements.txt and runtime.txt files. (Make sure to capitalize the P of Procfile otherwise Heroku might not recognise it!):

    Then, fill in the codes below:

        release: python manage.py migrate
        web: gunicorn DJANGO_PROJECT_NAME.wsgi --log-file -
    
  2. Deploy the Web App

    Once all the previous steps are completed, we need to collect all the static files using python manage.py collectstatic command. A staticfiles folder should be created.

    After that, we are ready to finally commit and push all changes:

        $ git add .
        $ git commit -m "blah blah blah"
        $ git push heroku master
    

    After the build is done and your app has been released, visit YOUR-APP-NAME.herokuapp.com

IMPORTANT NOTE:

  1. When deploying to Heroku, make sure that your migrations folder are not included inside .gitignore! Heroku will need the migration files to update the PostgreSQL database.

  2. If you encounter 500 Server Error issues in only the following cases:

* Debug=True && DB=local => Runs fine
* Debug=True && DB=production => Runs fine
* Debug=False && DB=local => Runs fine
* **Debug=False && DB=Production => 500 Server Error**

First, try including the following code in settings.py to display logging messages:

import logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
        },
    },
}

Then, try refreshing the site. Once the same error occurs, run $ heroku logs --tail inside terminal, the log will tell you the error message caused. This is because this problem is commonly caused by missing static files. By running said command, we can see what static files cannot be found.

31