Brenton Cleeland

Deploying Django apps with Github Actions and Django Up

Published on

For the last few years I've been using Django Up to deploy my small Django side projects. It's implemented as a Django management command that under the hood uses Ansible to deploy to an Ubuntu 22.04 server.

In the past, I've typically run deployments from my local machine. However, this year I've been encouraging more teams to focus on CI/CD practices, so I've decided to migrate a number of projects to deploy from Github Actions (thanks, work!).

With that in mind, I wanted to share a quick guide on how to deploy a new Django project to a VPS using Github Actions.

Quick note: you need to replace "test.django-up.com" with your domain in all the examples below.

Create a VPS using Ubuntu 22.04

To keep things simple, Django Up only supports a single version of Ubuntu LTS. However, this shouldn't be a problem, as you can always continue using an older version if you're unable to upgrade your server's version in the future. Django Up is actually intended to be copied into your project so that you can make any changes you want locally.

If you don't already have an account with a VPS provider, I can recommend Linode (affiliate link1), Digital Ocean, and Vultr.

When creating a new VPS select Ubuntu 22.04 and make sure to select the option that configures an SSH key from your account. With any of these providers, the $5/mo option should be sufficient for a small project.

Set up your domain name

Once you have your VPS up and running, copy the IP address and configure your domain name to point to it. You'll need to follow the instructions of your DNS provider to do this.

For example, in my case, I have "test.django-up.com" configured in Gandi's DNS Management tool, as shown in the following image:

DNS configuration on Gandi.net

To test that your VPS and domain are configured correctly, try SSH'ing to the server as the root user:

ssh root@test.django-up.com

Create and add an SSH key for deployments

To deploy your Django project using Github Actions, you'll need to create a separate SSH key specifically for use during the deployment process. A good resource for generating a new SSH key is the Generate a new SSH key guide from Github.

On your local machine, run the following command to generate an SSH key using ssh-keygen:

ssh-keygen -t ed25519 -C "deploy@test.django-up.com"

When prompted to save the key, enter a filename such as "deploy_key" (do not use the default filename, as you don't want this key to be saved in the .ssh directory).

Next, copy the public key (e.g., "deploy_key.pub") into the .ssh/authorized_keys file on the server. You can use the ssh-copy-id tool to do this:

ssh-copy-id -i deploy_key.pub root@test.django-up.com

Alternatively, you can simply copy the public key into a new line in the .ssh/authorized_keys file on the server.

We will use the private key in a few minutes when configuring the secrets for our Github Action.

Create a repository on Github

While it's great that there are a variety of different git hosting providers, this guide is specifically for Github and it's Actions feature. Create a new repository on Github with any name you like. For my example project I've used post-django-action.

We'll push to this repository once we have our project set up in the next step.

Set up a Django project

These instructions come directly from the Django Up README and provide a step-by-step guide for setting up a new Django project and preparing it for deployment using Django Up. Here is a summary of the steps outlined in the instructions:

  1. Create a directory for your new project and cd into it:
mkdir testproj
cd testproj
  1. Install Django, PyYAML, and dj_database_url using pipenv:
pipenv install Django pyyaml dj_database_url
  1. Start a new Django project using pipenv:
pipenv run django-admin startproject testproj .
  1. Initialize the new project as a git repository:
git init
  1. Add Django Up as a git submodule:
git submodule add git@github.com:sesh/django-up.git up
  1. Add Django Up to your INSTALLED_APPS in settings.py to enable the management command:
INSTALLED_APPS = [
  # ...
  'up',
]
  1. Add your target domain to the ALLOWED_HOSTS in settings.py:
ALLOWED_HOSTS = [
  'test.django-up.com',
  'localhost'
]
  1. Set the SECURE_PROXY_SSL_HEADER in settings.py to ensure the connection is considered secure:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_SCHEME', 'https')
  1. Set up your database to use dj_database_url:
import dj_database_url
DATABASES = {
    'default': dj_database_url.config(default=f'sqlite:///{BASE_DIR / "db.sqlite3"}')
}
  1. Generate a new secret key and configure your application to pull it out of the environment:

In .env:

DJANGO_SECRET_KEY=<your unique secret key>

And in your settings.py replace the existing SECRET_KEY line with this:

import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
  1. Create a requirements file based on your pipenv:
pipenv lock -r > requirements.txt

Add a simple view

To make sure that your Django project is working correctly when it's deployed, you can add a simple test view to your urls.py file. Here is an example of how you could do this:

from django.urls import path
from django.http import HttpResponse

def test(request):
    return HttpResponse("Hey, it's working!")

urlpatterns = [
    path("", test),
]

This will create a view that displays the message "Hey, it's working!" when the root URL of your site is accessed. While there are many other features and functionality in Django that you could disable for this example, it's probably best to leave them alone for now.

Commit and push our project

Before committing and pushing your project to a Github repository, it's a good idea to add a .gitignore file to exclude some private and generated Python files. In the root of your project, you can create a .gitignore file with the following contents:

.env
*.py[cod]
__pycache__/
db.sqlite3

This will ensure that the .env file, compiled Python files, the __pycache__ directory, and the SQLite database file are not included in your repository.

To add the repository you created earlier as the origin, you can use the following command, which is provided on the blank repository page on Github:

git remote add origin <url>

Commit these changes and push them to a new Github repository.

git add .
git commit -m "Stand by for deployment"
git push -u origin main

Set up our Repository Secrets

We're taking advantage of Github's Repository Secrets feature to manage our production SECRET_KEY and SSH Key. You can use this feature to manage any other secrets or environment specific configuration that your application has.

To do this head to you Github repository's settings, then select "Actions" under the "Secrets" section on the left.

You'll need to add two secrets. DJANGO_SECRET_KEY should be a securely generated secret key. You can use an online utility to generate this, or use the Python secrets module.

DEPLOY_SSH_KEY is the private key that we generated above. It's in the "deploy_key" file and should start with -----BEGIN OPENSSH PRIVATE KEY-----.

Once configured it should look like this:

Add the Github Action

Phew, that's quite a bit of work to get to this point, luckily most of that should feel pretty familiar. And now we get to the Actions.

To use Github Actions, you will need to create a .github/workflows directory within your project, and then create a deploy.ymlfile within that directory. This file will contain the instructions for running the deployment job.

mkdir -p .github/workflows
touch .github/workflows/deploy.yml

If you're not familiar with Github Actions and the Workflows syntax I recommend checking out the official guide.

Since this is a deployment action we only want to allow it to run on our main branch or when triggered manually. We can configure that in our on block with. Take the empty deploy.yml file and add the following to get us started:

name: Deploy

on:
  push:
    branches: [ main ]
  workflow_dispatch:

Our deploy.yml only has a single job. We checkout the code, install dependencies, setup up the environment, configure the SSH key and deploy the application in the snippet below. Add this below the on section, then we're ready to test our deployment (note the domain that you will need to change!):

jobs:
  django-deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
      with:
        submodules: recursive

    - uses: actions/setup-python@v4
      with:
        python-version: '3.10'

    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install pipenv
        pipenv install

    - name: Create .env
      run: |
        echo "DJANGO_SECRET_KEY=$DJANGO_SECRET_KEY" > .env
      env:
        DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }}

    - name: Setup SSH key
      uses: shimataro/ssh-key-action@v2
      with:
        key: ${{ secrets.DEPLOY_SSH_KEY }}
        known_hosts: unnecessary

    - name: Accept host fingerprint
      run: |
        ssh -o StrictHostKeyChecking=accept-new root@test.django-up.com "ls"

    - name: Run up
      run: |
        pipenv run python manage.py up test.django-up.com --email=<your-email>
      env:
        DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }}

    - name: Remove .env
      run: rm .env

After all that, the workflow itself is a little underwhelming, right?

First it checks out the code from the repository, including any submodules, and sets up Python with the specified version using Github's provided actions. It then installs the necessary dependencies and creates a .env file with the DJANGO_SECRET_KEY secret as a variable. This .env file is automatically loaded by by pipenv and Django Up.

Next, the action sets up an SSH key using the DEPLOY_SSH_KEY secret, and accepts the host fingerprint of the server by running a command through SSH. Finally, it runs the up management command for Django Up, passing in the target domain and email address as arguments. To clean up we remove the .env file.

All of the secrets and environment variables are configured in the repository settings. If your project needs other variables you can add them in the same way that we added the DJANGO_SECRET_KEY.

Push to Github

Commit the .github directory and push the updated main branch to Github.

git add .github
git commit -m "Off to production we go 🚀"
git push

This will automatically kick off the deployment workflow for the first time. You can check it's progress on the "Actions" tab of your repository.

Test your deployment

That's it! You should now be able to see your working site at your domain.

You can view my final repository at sesh/post-django-actions. That deploys the site at test.django-up.com.

I'm also deploying https://django-up.com using this method, and it should automatically redirect to the Github repository. You can view the slightly modified workflow that supports multiple stages on Github.

  1. By using this link you will receive a $100, 60-day credit once a valid payment method is added. If you spend $25 I will receive $25 credit in my account.