Get Started with Django on Scalingo

Requirements

Before doing this tutorial you should have setup your environment:

Create Your Application and Databases on Scalingo

$ scalingo create myapp
  • Go on the dashboard of your application.
  • Select the Addons category
  • Choose the database you want to use PostgreSQL®, MySQL®…

Initialize Your Application locally

On your computer, start a Django website.

# Create your project
mkdir my-project
cd my-project

# use pipenv to handle dependancies (recommanded for python buildpack)
pip install --user pipenv

# Install Django and other useful packages:
pipenv install django django-environ psycopg gunicorn dj-static

# enter the virtual environment
pipenv shell

# Initialize Django project, this command will
# create a manage.py file and a myapp directory
django-admin startproject myapp .

Define How To Start Your Application

You need to specify how to start the applicative server, and define it to display logs on stdout to fit the 12-factor principles.

This is achieved by creating a file named Procfile at the root of the project with a web entry.

Scalingo handles WSGI and ASGI server :

web: gunicorn myapp.wsgi --log-file -
web: gunicorn myapp.asgi --worker-class=uvicorn.workers.UvicornWorker --log-file -

Start script

A better practice is to use a start script, usually stored in the bin directory. This gives you more flexibility on how to start your app.

First, add bin/start.sh, with:

#!/bin/bash

gunicorn myapp.wsgi --log-file -

Then update Procfile:

web: bash bin/start.sh

Application Configuration

The configuration of the application has to be done through the environment variables, no credentials should be present statically in the code. It is usually a bad practice. It’s also a recommandation of the 12-factor principles.

We use django-environ to ease this part.

The configuration file in our example is located at myapp/settings.py.

Ensure That the Base Directory of the Application Is Defined

Add (if not already set) the following definition after the top comment header of the myapp/settings.py file:

import environ
from pathlib import Path

root = environ.Path(__file__) - 2  # get root of the project
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(root())

# handle .env file for local development
env_path = BASE_DIR / ".env"
if env_path.is_file():
    environ.Env.read_env(str(env_path))  # reading .env file

# use an environment variable to set a secret and fetch it with django-environ
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env.str("SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)

Configure the Database Access

Still in the myapp/settings.py, replace:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

With:

DATABASES = {
    "default": env.db("DATABASE_URL", default=f"file:///{BASE_DIR / 'db.sqlite3'}"),
}

This has no effect on the default behavior. If no DATABASE_URL has been set, the application will fallback on your development backend, sqlite3. However, we strongly advise you to use the same database in development and in production to ensure bug free migrations.

Static File Serving

In your settings configuration file myapp/settings.py:

STATIC_ROOT = 'staticfiles'
STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

In the myapp/wsgi.py file, replace

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

With

from django.core.wsgi import get_wsgi_application
from dj_static import Cling

application = Cling(get_wsgi_application())

Cling is part of the dj_static module and is designed to serve static files.

If you don’t need to serve static files, you can setup the following environment variable:

DISABLE_COLLECTSTATIC=1

It will be taken into account during the next deployment of your application.

Configuration of Allowed Hosts

By default, Django will only serve requests coming for specific domain names to prevent HTTP Host header attacks. Without specifying these allowed domains, you will get the following error:

> Invalid HTTP_HOST header: '<domain>'. You may need to add '<domain>' to ALLOWED_HOSTS.

Consequently, you need to modify the settings.py file. Replace this static array:

ALLOWED_HOSTS = ["localhost"]

By this dynamic block reading the environment:

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["127.0.0.1", "localhost"])

Then change the environment variable of your application with a coma-separated list of domains which will be used to access the app.

scalingo --app app-name env-set ALLOWED_HOSTS=app-name.osc-fr1.scalingo.io,example.com

Deploy your application

Your app is almost done, now we need to ship it. Django requires that severals operation are done each time we deploy the app. In this chapter we will see how we can do that on Scalingo.

In Python buildpack, there are 3 hooks we can use: pre-compile, post-compile and postdeploy. Each is triggered at a specific time and will have some use.

Automatic Database Migrations

If you use Django’s ORM and migrations files, you want to use postdeploy hook to trigger the migrate command.

Database migrations in Django are separated in two steps:

  • python manage.py makemigrations, which creates migrations files from model change
  • python manage.py migrate, which applies the migration file to the related database

Because makemigrations modifies the source code, this command must be done on your computer, and the created migration files committed to Git. This is not a command to execute in your postdeploy hook.

The migrate command is the one to execute to apply to migrations to your database. You can add it to your Procfile like this:

First create a file bin/post_deploy.sh with:

#!/bin/sh

# Execute structure migrations
python manage.py migrate users
python manage.py migrate

After, activate the hook by adding following line in Procfile:

postdeploy: bash bin/post_deploy.sh

This triggers a one-off container just after finishing compiling but before replacing current app (if any were running when triggering the deployment).

Collect static

Previous hook is not good for collect static because the script is run in a one-off which is a copy of the final container. Therefore all files manipulation would be available only on the one-off and not in the actual container which will serve web request.

To trigger script on the actual container, you will need to use post-compile hook.

Add a file bin/post_compile with (note the missing .sh extension!):

#!/bin/sh

export PYTHONPATH=/build/${REQUEST_ID}/.apt/usr/lib/python3/dist-packages/:${PYTHONPATH}

# collect static
python manage.py collectstatic --noinput

Compile .mo Message Translation File

If you are using compilemessages command to generate .mo file for gettext translations, that command needs to run at the same stage as collect static.

Add following line to bin/post_compile file:

python manage.py compilemessages

Save Your App With Git and Automatic Deployment

Setup .gitignore

You don’t want to keep track of everything on your version control system. To prevent such files to be added to Git, create the file .gitignore at the root of your project with the following content:

venv
*.pyc
staticfiles
.env

Commit Your Application

git init
git add .
git commit -m "Init Django application"

Deploy Your Application

git remote add scalingo git@ssh.osc-fr1.scalingo.com:myapp.git
git push scalingo master

Note that the remote URL depends on the region of your application. You can get it using our CLI with:

scalingo --app myapp git-show

Each time you push on the master branch, it should trigger a deployment job on Scalingo.

Access Your Application

The URL is displayed at the end of the deployment logs. It won’t change.

…
Waiting for your application to boot...
<-- https://myapp.osc-fr1.scalingo.io -->

Suggest edits

Get Started with Django on Scalingo

©2024 Scalingo