Aug. 25, 2022

Deploy a Django + Tailwind + daisyUI application to Heroku

To use as an example, we will start from a new project already setup with Django + Tailwind + daisyUI available on github.

git clone https://github.com/kenshuri/setup_django_tailwind_daisyui.git 005_deploy_to_heroku
cd 005_deploy_to_heroku

Deploy to Heroku

Create a Heroku Remote

heroku create -a deploy-to-heroku-blog-example
git remote -v

Deploy the code

git push heroku main

But it won't be so easy! You should see the below error message:

remote:  !     Error while running '$ python manage.py collectstatic --noinput'.
remote:        See traceback above for details.
remote:
remote:        You may need to update application code to resolve this error.
remote:        Or, you can disable collectstatic for this application:
remote:
remote:           $ heroku config:set DISABLE_COLLECTSTATIC=1
remote:
remote:        https://devcenter.heroku.com/articles/django-assets
remote:  !     Push rejected, failed to compile Python app.
remote: 
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !       Push rejected to deploy-to-heroku-blog-example.
remote:
To https://git.heroku.com/deploy-to-heroku-blog-example.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/deploy-to-heroku-blog-example.git'

It's very normal as Django does not support serving static files in production. A solution is proposed below:

Django and static assets in production

Follow instructions available on that page. To sum-up, you'll need to:

  • Install whitenoise
pip install whitenoise
  • Add the following to your settings.py
# settings.py

# The absolute path to the directory where collectstatic will collect static files for deployment.
STATIC_ROOT = BASE_DIR / "staticfiles"
  • Edit your settings.py file and add WhiteNoise to the MIDDLEWARE list. The WhiteNoise middleware should be placed directly after the Django SecurityMiddleware (if you are using it) and before all other middleware:
# settings.py

MIDDLEWARE = [
    # ...
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    # ...
]

Now, you could commit and try again to deploy your app to Heroku...

git push heroku main

The deployment is successful!! Now go and check your app... What the heck??!! Something is wrong and you should see an error message saying:

Application error

An error occurred in the application and your page could not be served. If you are the application owner, check your logs for details. You can do this from the Heroku CLI with the command

heroku logs --tail

Again, this was to be expected, we're not done yet! At the moment, as indicated by the error message, there is no web process running.

Add a Procfile and setup gunicorn

As described here, Heroku web applications require a Procfile. This file is used to explicitly declare your application’s process types and entry points. It is located in the root of your repository.

The Procfile uses gunicorn, we thus need to install it following the instructions available on this page. As a summary:

  • Install gunicorn
pip install gunicorn
  • Add a Procfile to project root containing simply the following. The name of the app (before .wsgi below) is from the value of the WSGI_APPLICATION variable define in your settings.py.
Procfile

web: gunicorn blogProject.wsgi
  • Update your requirements.txt to add gunicorn
pip freeze > requirements.txt
  • Commit and push to heroku

We're not there yet ! You should see an error message complaining about DisallowedHost at /

Add ALLOWED_HOSTS

You need to specify in your settings.py that your application should accept connection coming from your application domain... To do so, modify the variable ALLOWED_HOSTS defined in your settings.py. In my case, in need to do:

# settings.py

ALLOWED_HOSTS = ['deploy-to-heroku-blog-example.herokuapp.com']

Commit again, and push to heroku!

Bravoooooo!!!!

You should see the landing page :) But wait, something is not right...

...

...

...

The design is wrong!! Tailwind and daisyUI are not used!!!! This is the worst tutorial ever :'(

No worries, we're almost there! We just need to add the css file to our git repo! Run the following commands:

cd jstoolchains
npm run tailwind-build

This will automatically creates a minified css file in your app static folder. Finally, commit and push to heroku...

Finally!! It should be all good now ;)

(Optionnal - Security Warnings) DEBUG mode and SECRET_KEY

In your setttings.py file, two settings should get your attention, see below:

# settings.py

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-blablabla'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = 'True'

Well, actually, the SECURITY WARNING comment in CAPITAL letters should! These are the critical settings listed in django deployment checklist Let's take care of these warnings 💪

DEBUG mode

The DEBUG mode is enable by default though the seeing DEBUG when you start a new django project. As its name suggests, it is useful when you are debuging as it shows useful information when a bug occurs. However, you do not want to have this mode actovated in Production as it could give unsafe information to someone with bad intentions 👿

Let's code a logic so that it is enabled by default when you are working locally and disabled otherwise. To do so, we will rely on the package python-dotenv. Let's install it:

pip install python-dotenv

Then, create a .env file at your project root containing the following:

.env <- That's just the name of the file repeated here

DJANGO_DEBUG=True

Finnaly, modify your settings.py file so that the .env file settings are read.

# settings.py

import os
from dotenv import load_dotenv
load_dotenv()
DEBUG = os.environ.get("DJANGO_DEBUG") == 'True'

Thanks to this piece of code, the DEBUG variable is True if DJANGO_DEBUG is True, False otherwise. You can know deploy your modified app to Heroku!

If you now try to acces a non-existing page on your website, you see an error page with addtional information because the DEBUG is activated: indeed, your app load the DJANGO_DEBUG config in your .env file.

By default, load_dotenv doesn't override existing environment variables, so to make sure that the DEBUG mode is disabled in PRODUCTION, you'll need to add the DJANGO_DEBUG = False to your app environment on Heroku. You can do so by going in you App settings, then Config Vars 🙂. If you try again to acces a non-existing page on your website, you should see a regular error message!

SECRET_KEY

As suggested in the comment in your settings.py, the secret key used in production should be kept secret! Building on the same .env used before, we will create 2 separate SECRET_KEY

  • 1 used in production, saved as a Config Var in your app settings on Heroku
  • 1 used for development, saved in your .env file

To create secret keys, the easiest is to use the django built-in function get_random_secret_key

# Python console

from django.core.management.utils import get_random_secret_key
get_random_secret_key()

Then in your settings.py change the secret key value so that it uses environment variable.

# settings.py

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")

Bravo!! You can now safely deploy your app to Heroku! 👏

Source code

The final version of this tutorial code is available on github!