Running Django on DigitalOcean's App Platform

Last updated October 15th, 2020

DigitalOcean's new App Platform is a Platform-as-a-Service (PaaS) offering, which (much like Heroku) allows you to deploy an application from a git repository.

This article looks at how to deploy a Django application to DigitalOcean's App Platform.

Contents

Why Use DigitalOcean's App Platform?

The goal of DigitalOcean's App Platform is to simplify deployment so you can focus on application development instead of managing infrastructure. It's perfect for your everyday, run-of-the-mill Django app with Postgres (or MySQL) for persistence, and Redis for your Celery message broker. That said, if your app does require you to have access to the underlying infrastructure, you'll probably want to steer away from App Platform and use an Infrastructure-as-a-Service (IaaS) solution like a DigitalOcean Droplet instead.

Positives:

  1. Immutable deploys
  2. Zero downtime deployments
  3. Built in continuous deployment
  4. Cloudflare CDN by default
  5. Vertical and horizontal scaling (no auto scaling yet)
  6. Supports Docker

Negatives:

  1. Poorly documented
  2. Compared to Heroku, it has few extensions (like logging and monitoring services)

Yes, it's more expensive than an IaaS solution, but if your app doesn't have complex infrastructure requirements then you'll save time and money since you won't need to hire an ops engineer.

Project Setup

As of writing, DigitalOcean's App Platform only works with git repositories hosted on GitHub. We'll be deploying a basic Django app. If you'd like to follow along you can use either a pre-created demo app or your own app.

Demo App

Feel free to clone down the demo app from the v1 branch of the digitalocean-app-platform-django repo:

$ git clone https://github.com/testdrivenio/digitalocean-app-platform-django --branch v1
$ cd digitalocean-app-platform-django
$ git checkout -b master

Make sure to create a new GitHub repo and push the code up to the master branch.

Your App

If you have your own Django app that you'd like to deploy, for simplicity's sake, update the following config in your settings.py file:

SECRET_KEY = 'please-change-me'
DEBUG = True
ALLOWED_HOSTS = ['*']

After the initial deploy, we'll look at how to change each of these so your app is more secure.

Also, add the following static asset config:

STATIC_URL = '/static/'
STATIC_ROOT = Path(BASE_DIR).joinpath('staticfiles')
STATICFILES_DIRS = (Path(BASE_DIR).joinpath('static'),)

DigitalOcean

Let's set up DigitalOcean to work with our application.

First, you'll need to sign up for a DigitalOcean account (if you don't already have one). Next, from the DigitalOcean dashboard, click on the link for "Apps" in the sidebar and then link your GitHub account. You'll probably want to grant access to only the single repo created earlier in this article rather than for all repos:

Authorize GitHub

Once done, select the repo and click "Next":

Select GitHub Repo

Give your app a name and select the region. Feel free to stick with the defaults -- repo name and New York, respectively. Keep "Autodeploy code changes" checked for continuous deployment.

Click "Next".

Name and Region

On the app configuration page, DigitalOcean should have already detected that the app is Python-based (based on the presence of the requirements.txt file).

Under the hood, DigitalOcean uses Cloud Native Buildpacks to run your application.

Change the run command to gunicorn hello_django.wsgi:application --bind 0.0.0.0:8080 --worker-tmp-dir /dev/shm and click "Next":

Configure App

Stick with the "Basic" plan and launch the app.

On the next screen, under the "Deployments" tab, click the "Details" link to view the live deployment logs:

Deployment Logs

It should take a few minutes to build and deploy. Once complete, click the "Live App" link in the splash message to open your running app in a new browser tab:

Deployment Complete

You should see:

{
  "hello": "world!"
}

Database

Next, rather than using SQLite, let's configure Postgres.

From your app's dashboard, click "Components". Then, click the "Create Component" dropdown and select "Database":

Add Database

Stick with the default "Dev Database" named "db". Click "Create and Attach".

This will automatically configure a DATABASE_URL environment variable and redeploy the app. This will take around ten minutes, since the database needs to be configured. Once the deployment is done, you can view the environment variable from your console:

Add Database

Add the following variable to the settings file to read the environment variable:

DATABASE_URL = os.getenv('DATABASE_URL', None)

Next, update the database config like so to use the DATABASE_URL if it exists and configure Postgres:

if not DATABASE_URL:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': BASE_DIR / 'db.sqlite3',
        }
    }
else:
    db_info = urlparse(DATABASE_URL)
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql_psycopg2',
            'NAME': 'db',
            'USER': db_info.username,
            'PASSWORD': db_info.password,
            'HOST': db_info.hostname,
            'PORT': db_info.port,
            'OPTIONS': {'sslmode': 'require'},
        }
    }

Add the appropriate imports to the top:

import os
from urllib.parse import urlparse

Add psycopg2 to the requirements:

psycopg2-binary==2.8.6

Make sure it works locally. Commit and push your code up to GitHub to trigger a new deploy.

Finally, after the deployment completes, jump back into "Console" to apply your migrations (via python manage.py migrate) and create a superuser (via python manage.py createsuperuser):

Apply Migrations and Create Superuser

You can also verify that the Postgres config is being used:

Verify Postgres

$ python manage.py shell

>>> from django.conf import settings
>>> settings.DATABASES

Environment Variables

Moving on, let's harden the app a bit by configuring the following settings via environment variables:

  1. SECRET_KEY
  2. DEBUG
  3. ALLOWED_HOSTS

Update the following variables in settings.py:

SECRET_KEY = os.getenv('SECRET_KEY', 'please-change-me')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '127.0.0.1,localhost').split(',')

Commit your code again and push it up to GitHub to trigger another new deploy.

After the deploy completes, click "Components" and then the "edit" link within the "Environment Variables" row:

Add Environment Variables

Then, add the variables:

  1. SECRET_KEY - [email protected]%9$#3#)9cff)jdcl&[email protected](u!y)[email protected]!e2s8!(jrrta_u+
  2. DEBUG - False
  3. ALLOWED_HOSTS - digitalocean-app-platform-django-w74gn.ondigitalocean.app

Be sure to generate a new secret key and replace digitalocean-app-platform-django-w74gn.ondigitalocean.app with your domain.

After updating, the app will automatically redeploy.

Static Files

We'll use WhiteNoise to manage static assets.

Add it to the requirements.txt file:

whitenoise==5.2.0

Update the MIDDLEWARE list in settings.py:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # new
    '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',
]

Then, to enable compression and caching support, add:

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

Commit. Push to GitHub. Once the deploy completes, make sure the static assets work on the admin page.

Celery

Although we won't demo how to set up Celery in this post, DigitalOcean makes it easy to configure Redis, which can then be used as your message broker and result backend. You can then set up the Celery worker as a "Worker" component:

Worker Component

Development Workflow

With autodeploy enabled, any time changes to the master branch are added to source control on GitHub, the app will redeploy automatically. With zero-downtime enabled by default, end users will not experience any downtime when redeploys occur.

It's best to use some flavor of the feature-branch workflow so that you can check in code to GitHub on a different branch than master so that you can test your app before the changes go live on production.

Example:

  1. Create a new branch
  2. Make changes to the code
  3. Commit and push your code to GitHub to trigger a CI build via GitHub Actions
  4. Open a PR against the master branch
  5. Merge once the CI build completes to update production

Conclusion

This post took a quick look at how to run a Django application on DigitalOcean's App Platform.

In general, App Platform is a powerful PaaS solution that you should definitely evaluate for your Django applications along with other solutions like:

  1. Heroku
  2. PythonAnywhere
  3. AWS Elastic Beanstalk
  4. Google App Engine
  5. Render

The major downside at this point to App Platform is that it lacks some of the features and extensions that some of the other PaaS solutions provide. That said, according to this post there are a number of new features in the works like scheduled jobs, deployment previews, VPC integration, and deployment rollbacks.

If you do decide to use App Platform, be sure to:

  1. Review how to leverage vertical and horizontal scaling
  2. Configure an HTTP-based health check
  3. Set up a production-grade Managed Database as well as Redis

Make sure to delete your Django app along with the database so you don't incur any additional charges.

You can find the final code in the digitalocean-app-platform-django repo.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.