Django

The package dockerflow.django package implements various tools to support Django projects that want to follow the Dockerflow specs:

  • A Python logging formatter following the mozlog format to be used in the LOGGING setting.

  • A middleware to emit request.summary log records based on request specific data.

  • Views for health monitoring:

    • /__version__ - Serves a version.json file

    • /__heartbeat__ - Run Django checks as configured in the DOCKERFLOW_CHECKS setting

    • /__lbheartbeat__ - Retuns a HTTP 200 response

  • Signals for passed and failed heartbeats.

See also

For more information see the API documentation for the dockerflow.django module.

Setup

To install python-dockerflow’s Django support please follow these steps:

  1. Add dockerflow.django to your INSTALLED_APPS setting

  2. Define a BASE_DIR setting that is the root path of your Django project. This will be used to locate the version.json file that is generated by CircleCI or another process during deployment.

    See also

    Versions for more information

  3. Add the DockerflowMiddleware to your MIDDLEWARE_CLASSES or MIDDLEWARE setting:

    MIDDLEWARE_CLASSES = (
        # ...
        'dockerflow.django.middleware.DockerflowMiddleware',
        # ...
    )
    
  4. Configure logging to use the JsonLogFormatter logging formatter for the request.summary logger (you may have to extend your existing logging configuration!).

Configuration

Accept its configuration through environment variables.

There are several options to handle configuration values through environment variables, e.g. as shown in the configuration grid on djangopackages.com.

os.environ

The simplest is to use Python’s os.environ object to access environment variables for settings and other variables, e.g.:

MY_SETTING = os.environ.get('DJANGO_MY_SETTING', 'default value')

The downside of that is that it nicely works only for string based variables, since that’s what os.environ returns.

python-decouple

A good replacement is python-decouple as it’s agnostic to the framework in use and offers casting the returned value to the type wanted, e.g.:

from decouple import config

MY_SETTING = config('DJANGO_MY_SETTING', default='default value')
DEBUG = config('DJANGO_DEBUG', default=False, cast=bool)

As you can see the DEBUG setting would be populated from the DJANGO_DEBUG environment variable but also be cast as a boolean (while considering the string values '1', 'yes', 'true' and 'on' as truthy values, and similar for falsey values).

django-environ

Django-environ follows similar patterns as python-decouple but implements specific casters for typical Django settings. E.g.:

import environ
env = environ.Env()

MY_SETTING = env.str('DJANGO_MY_SETTING', default='default value')
DEBUG = env.bool('DJANGO_DEBUG', default=False)
DATABASES = {
    'default': env.db(),  # automatically looks for DATABASE_URL
}

django-configurations

If you’re interested in even more complex scenarios there are tools like django-configurations which allows loading different sets of settings depending on an additional environment variable DJANGO_CONFIGURATION to separate settings by environment (e.g. dev, stage, prod). It also ships with Value classes that implement configuration parsing from environment variable and casting, e.g.:

from configurations import Configuration, values

class Dev(Configuration):
    SESSION_COOKIE_SECURE = False
    DEBUG = values.BooleanValue(default=False)

class Prod(Dev):
    SESSION_COOKIE_SECURE = True

In that example the configuration class that is given in the DJANGO_CONFIGURATION environment variable would be used as the base for Django’s settings.

PORT

Listen on environment variable $PORT for HTTP requests.

Depending on which WSGI server you are using to run your Python application there are different ways to accept the PORT as the port to launch your application with.

It’s recommended to use port 8000 by default.

Gunicorn

Gunicorn automatically will bind to the hostname:port combination of 0.0.0.0:$PORT if it find the PORT environment variable. That means running gunicorn is as simple as using this:

gunicorn myproject.wsgi:application --workers 4 --access-logfile -

See also

The full gunicorn documentation for more details.

uWSGI

For uWSGI all you have to do is to bind on the PORT when you define the uwsgi.ini, e.g.:

[uwsgi]
http-socket = :$(PORT)
master = true
processes = 4
module = myproject.wsgi:application
chdir = /app
enable-threads = True

See also

The full uWSGI documentation for more details.

Versions

Must have a JSON version object at /app/version.json.

Dockerflow requires writing a version object to the file /app/version.json as seen from the docker container to be served under the URL path /__version__.

To facilitate this python-dockerflow contains a Django view to read the file under path BASE_DIR + 'version.json' where BASE_DIR is required to be defined in the Django project settings, e.g.:

import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Assuming that the settings.py file is contained in the project folder That means the BASE_DIR setting will be the one where the manage.py file is located in the below example directory tree:

 .
 ├── .dockerignore
 ├── .gitignore
 ├── Dockerfile
 ├── README.rst
 ├── circle.yml
 ├── manage.py
 ├── requirements.txt
 ├── staticfiles
 │   └── ..
 ├── tests
 │   └── ..
 ├── version.json
 ├── myproject
 │   ├── app1
 │   │   ├── ..
 │   │   └── ..
 │   ├── app2
 │   │   ├── ..
 │   │   └── ..
 │   ├── settings.py
 │   └── urls.py
 └── ..

Health monitoring

Health monitoring happens via three different views following the Dockerflow spec:

GET /__version__

The view that serves the version information.

Example request:

GET /__version__ HTTP/1.1
Host: example.com

Example response:

HTTP/1.1 200 OK
Vary: Accept-Encoding
Content-Type: application/json

{
  "commit": "52ce614fbf99540a1bf6228e36be6cef63b4d73b",
  "version": "2017.11.0",
  "source": "https://github.com/mozilla/telemetry-analysis-service",
  "build": "https://circleci.com/gh/mozilla/telemetry-analysis-service/2223"
}
Status Codes:
GET /__heartbeat__

The heartbeat view will go through the list of configured Dockerflow checks in the DOCKERFLOW_CHECKS setting, run each check, and, if settings.DEBUG is True, add their results to a JSON response.

The view will return HTTP responses with either a status code of 200 if all checks ran successfully or 500 if there was one or more warnings or errors returned by the checks.

Custom Dockerflow checks:

To write your own custom Dockerflow checks, please follow the documentation about Django's system check framework and particularly the section “Writing your own checks”.

Note

Don’t forget to add the check additionally to the DOCKERFLOW_CHECKS setting once you’ve added it to your code.

Example request:

GET /__heartbeat__ HTTP/1.1
Host: example.com

Example response:

HTTP/1.1 500 Internal Server Error
Vary: Accept-Encoding
Content-Type: application/json

{
  "status": "warning",
  "checks": {
    "check_debug": "ok",
    "check_sts_preload": "warning"
  },
  "details": {
    "check_sts_preload": {
      "status": "warning",
      "level": 30,
      "messages": {
        "security.W021": "You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, your site cannot be submitted to the browser preload list."
      }
    }
  }
}
Status Codes:
GET /__lbheartbeat__

The view that simply returns a successful HTTP response so that a load balancer in front of the application can check that the web application has started up.

Example request:

GET /__lbheartbeat__ HTTP/1.1
Host: example.com

Example response:

HTTP/1.1 200 OK
Vary: Accept-Encoding
Content-Type: application/json
Status Codes:

Logging

Dockerflow provides a JsonLogFormatter Python logging formatter class.

To use it, put something like this in your Django settings file and configure at least the request.summary logger that way:

LOGGING = {
    'version': 1,
    'formatters': {
        'json': {
            '()': 'dockerflow.logging.JsonLogFormatter',
            'logger_name': 'myproject'
        }
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'json'
        },
    },
    'loggers': {
        'request.summary': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    }
}

Static content

To properly serve static content it’s recommended to use Whitenoise. It contains a middleware that is able to serve files that were built by Django’s collectstatic management command (e.g. including bundle files built by django-pipeline) with far-future headers and proper response headers for the AWS CDN to work.

To enable Whitenoise, please install it from PyPI and then enable it in your Django projet:

  1. Set your STATIC_ROOT setting:

    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    
  2. Add the middleware to your MIDDLEWARE (or MIDDLEWARE_CLASSES) setting:

    MIDDLEWARE_CLASSES = [
        # 'django.middleware.security.SecurityMiddleware',
        'whitenoise.middleware.WhiteNoiseMiddleware',
        # ...
    ]
    

    Make sure to follow the SecurityMiddleware.

  3. Enable the staticfiles storage that is able to compress files during collection and ship them with far-future headers:

    STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
    
  1. Install brotlipy so the storage can generate compressed files of your static files in the brotli format.

For more configuration options and details how to use Whitenoise see the section about Using WhiteNoise with Django in its documentation.

Settings

DOCKERFLOW_VERSION_CALLBACK

The dotted import path for the callable that returns the content to return under /__version__.

Defaults to 'dockerflow.version.get_version' which will be passed the BASE_DIR setting by default.

DOCKERFLOW_CHECKS

A list of dotted import paths to register during Django setup, to be used in the rendering of the /__heartbeat__ view. Defaults to:

DOCKERFLOW_CHECKS = [
    'dockerflow.django.checks.check_database_connected',
    'dockerflow.django.checks.check_migrations_applied',
]