Get Started with Django on Scalingo
Requirements
Before doing this tutorial you should have setup your environment:
-
Setup the SSH authentication, it has to be done to let you
push
your application on the platform. - Install ‘scalingo’ Command Line Interface
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
Using Daphne
It is also possible to use Daphne as an ASGI server. You can update the bin/start.sh
script described in the previous section to use Daphne to start the application:
#!/bin/bash
daphne --bind 0.0.0.0 --port $PORT myapp.asgi:application --proxy-headers
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 -->