The road to Docker, Django and Amazon ECS, part 3

For part 1

For part 2

Gunicorn in Docker

With Docker, our Django project is the only thing running. This means that we don't need gunicorn running in daemon mode and logging to files. We need gunicorn running in the foreground and logging to the console.

Setting up the gunicorn configuration

I found a great article with instructions on using gunicorn in Docker.

In summary, our Docker-specific gunicorn configuration is:

import os

for k, v in os.environ.items():
    if k.startswith("GUNICORN_"):
        key = k.split('_', 1)[1].lower()
        locals()[key] = v

This allows us to specify the gunicorn configuration using environment variables.

Setting up the gunicorn logging

The same article includes instructions about setting up logging. We added json-logging-py==0.2 to requirements.txt so the required library is included. Chances are, we will be changing all our logging to use this.

We added a docker_gunicorn_conf.py file to the repo:

[loggers]
keys=root, gunicorn.error

[handlers]
keys=console

[formatters]
keys=json

[logger_root]
level=INFO
handlers=console

[logger_gunicorn.error]
level=ERROR
handlers=console
propagate=0
qualname=gunicorn.error

[handler_console]
class=StreamHandler
formatter=json
args=(sys.stdout, )

[formatter_json]
class=jsonlogging.JSONFormatter

Environment variables

We know that we need to set at least these variables:

  • GUNICORNCONF
  • GUNICORN_WORKERS
  • GUNICORN_BACKLOG
  • GUNICORN_BIND
  • GUNICORN_ENABLE_STDIO_INHERITANCE

We will set their values in the Dockerfile

Running gunicorn

Spoiler alert! We will need a script that Docker will run as soon as it starts up the container. We want it to start gunicorn (and do a few other things), so we will create a new script: docker-entrypoint.sh

#!/bin/bash
python $HOMEDIR/manage.py collectstatic --noinput
python $HOMEDIR/manage.py migrate --noinput

#exec newrelic-admin run-program \
gunicorn \
    --log-config $HOMEDIR/conf/gunicorn_logging.conf \
    --config $HOMEDIR/conf/docker_gunicorn_conf.py \
    conf.wsgi:application

We chmod a+x docker-entrypoint.sh to make it executable.

In case you haven't guessed, $HOMEDIR is set in the Dockerfile.

Why are you running the collectstatic command here? Good question! You should be proud of yourself for the amount of awareness you maintain.

Initially the collectstatic command was in the Dockerfile, so it could be a part of the Docker image. However, whether it is by design, because of some of our code, or a third-party app, the command loads all the settings and apps and attempts to connect to the database when it runs. We can't have that.

At least it doesn't take long to run, and doesn't do anything if it has already run.

I'll explain more next time when I cover the Dockerfile creation.

Why are you running the migrate command here? Mostly because most of the example configurations for Docker and Django do this and I can't think of a better place to do it. Like the collectstatic command, it really only needs to be run once for the image, no matter how many machines use it. And also like the collectstatic command, we don't (necessarily) have a connection to the database to do this.

Also, it is a bit safer. In the worst case, a migration hasn't been applied, and the container will take a little longer to run the first time. In the best case, it doesn't need to do anything and starts up right away.

Next time

We'll conver creating the initial Dockerfile and testing it out to see if what we have done works.