Context
I am currently working on building a website for my wife to showcase her art. Apart from a simple static blog (this one) and some experiments with static pages and API development, the website I am about to build will be my first web development project. So I am learning a lot of things as I go. The plan is to use Django as the framework and host the site on Heroku to begin with, to keep things simple.
In this post I give a quick overview of the commands required to deploy a simple barebones Django project to Heroku. This is very much a proof-of-concept still, but it already makes me happy just to know the process is working as expected.
Pre-Requisites
- An existing Heroku account
- Can be created free of charge
- Having the Heroku CLI installed
Commands
Create a project folder, virtual environment and bootstrap the Django project:
# Create project folder
mkdir demo_project
cd demo_project
# Create virtual environment
python3 -m venv venv --copies
source venv/bin/activate
pip install django==3.2.5 django-heroku # Note: django-heroku is deprecated but still works..
# Set up Django Project
django-admin startproject demo_project . # Note: only lowercase letters and underscores allowed
Prepare the local repository and create a Heroku project:
# Initialise local git repo
git init
# Create `.gitignore`
curl https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,django > .gitignore
# Note: We used http://gitignore.io/ to dynamically generate a suitable .gitignore file
git status
git add .
git commit -m "Initial commit"
# Connect with Heroku account
heroku login
# Create Heroku Project
heroku create
# Note: Heroku will create a new project and assign a random name
# The Heroku CLI will also automatically configure the remote repo for us
# We can verify this with `git remote -v`
# To set a custom name use `heroku create django-heroku-demo-122021`
# Note: The name must be globally unique, and have a maximum of 30 characters
# Verify that the git remote is correctly configured
git remote -v
# Inspect the details of our freshly created Heroku application
heroku info
Adjust the Django configuration to make it compatible for deployment on Heroku:
###############################################
# Prepare Django app configuration for Heroku #
###############################################
echo "import django_heroku" >> demo_project/settings.py
echo "django_heroku.settings(locals())" >> demo_project/settings.py
echo "web: python manage.py runserver 0.0.0.0:\$PORT" > Procfile
pip freeze > requirements.txt
# Note: Later I would use `pip-compile` to track all sub-dependencies and their lineage explicitly
# Sample usage: `pip install pip-tools; echo "django" > requirements.in`
# And then: `pip-compile requirements.in; pip install -r requirements.txt`
# Configure ALLOWED_HOSTS
# Note: If we don't do this, the deploy will succeed but the deployed Django app will throw an error
# Step 1: Obtain the url of the application
app_url=`heroku apps:info -s | grep web_url`
echo $app_url
# Step 2: Extract only the relevant part of the url via sed and a regex capture group
app_url_parsed=`echo $app_url | sed -E 's|web_url=https:\/\/(.*)\/|\1|'`
echo $app_url_parsed
# Notes:
# Literal `/`characters need to be escaped with `\`
# I used `|` instead of `/` as the separator for clarity
# -e flag for "extended regular expression syntax" to allow for capture groups
# `\1` to reference the first capture group
# The general pattern is `sed 's/pattern/replacement/'
# Step 3: Add the app url to ALLOWED_HOSTS
# Replace ALLOWED_HOSTS config in settings.py and store result in temp_file
cat demo_project/settings.py | sed -e "s|ALLOWED_HOSTS = \[\]|ALLOWED_HOSTS = \[\'$app_url_parsed\'\]|" > temp_file
# Overwrite the settings with the changes
mv temp_file demo_project/settings.py
# Handle SECRET_KEY safely
# Note: Secrets should always be passed via environment variables
# Use `.env` file locally and `heroku config:set KEY=VALUE` for heroku
# Step 1: Generate a random secret and store it in .env
echo "SECRET_KEY=$(openssl rand -base64 32)" > .env
# Step 2: Obtain the secret as a shell variable
SECRET_KEY=`cat .env | sed -E 's|SECRET_KEY=(.*)|\1|'`
# Step 3: Set the secret as an environment variable with Heroku
heroku config:set SECRET_KEY="$SECRET_KEY"
# Step 4: Replace the initial secret key in the settings with an import from the environment variable
sed -i -e "s/.*SECRET_KEY.*/SECRET_KEY = os.environ['SECRET_KEY']/" demo_project/settings.py
# -i to make changes _in-place_
# The `os.environ['key']` syntax ensures an error is raised, if the secret is missing
# Step 5: Add `import os` to the top of the settings
sed -i '.bak' '1s/^/import os\'$'\n/g' demo_project/settings.py
# This generates a backup file `settings.py.bak` and modifies the orig. file in-place
# NOTE: placing the import at the _very top_ of the file is not good
# I rather would place it 'in the line above the secret key'
# But I couldn't quite get that to work on macOS with BSD sed
# For this demo having the import at the top of the file will suffice
rm demo_project/settings.py.bak # Remove backup
# Handle DEBUG variable
# We want to explicitly turn on or off the DEBUG setting
# Turn off by default
# Step 1: Make adjustment to load DEBUG setting from env, with False as Default
sed -i -e "s/.*DEBUG.*/DEBUG = os.environ.get('DEBUG', False)/" demo_project/settings.py
# Note: The `os.environ.get('key',<default>)` syntax sets a default if the key is missing
# Step 2: Add DEBUG setting to local .env file
echo "DEBUG=True" >> .env
# Step 3: Set the environment var for use with Heroku
heroku config:set DEBUG=False # Set explicitly to false
# heroku config:set `cat .env | grep DEBUG` # Same setting as in local .env
# Quick santiy-check
git diff
cat .env
heroku config
Commit changes and deploy to Heroku:
# Commit current state
git status
git add .
git commit -m "Prepare django project for demo deployment"
# Deploy project to Heroku
git push heroku master
# Note: Can use `git push heroku feature/demo:master` to deploy from another branch
# Open website and verify it is working
heroku open
And that's it. The basic Django app skeleton should now be accessible online. :-)
Cleaning up:
# Delete the demo application
heroku apps:destroy
# Confirm by typing in the name of the app
Next Steps
- Create an app with
python manage.py startapp my_app
- Configure the URL routes to include routes for the new app
- Create some views
- Create data model(s)
- Make migrations via
python manage.py makemigrations
- Run migrations via
python manage.py migrate
- Test locally via
python manage.py runserver
- Set up a proper database
- Handle static files
- Continue building out the site..
Bonus – Useful Heroku commands
heroku login
heroku create
heroku info
heroku status
heroku config
heroku config:set KEY=VALUE
heroku config:unset KEY
heroku apps
heroku apps:info -s # -s to get output suitable for shell
app_url=`heroku apps:info -s | grep web_url`
Reference / Further Reading
- Real Python Article
- Using the deprecated
django_heroku
package, but the process still works
- Using the deprecated
- Codementor (How to configure settings manually)
- Simplified Deployment with Django Buildpack
- Alternative deployment path that does all the configuration via heroku buildpack
- Note: I received an error that some required npm library is missing -> probably easy to fix
- Heroku Documentation on Django Deployment