Dockerizing Wagtail App


Michael Yin

Full Stack Developer

Last updated on Mar 16 2021


Table of Contents

Objectives

By the end of this chapter, you should be able to:

  1. Understand Docker Compose and the benefits.
  2. Use Docker Compose to create and manage Wagtail, Postgres services, and do development.

Install Docker Compose

Docker Compose is a tool for defining and running multi-container Docker applications. It uses YAML files to configure the application's services and performs the creation and start-up process of all the containers with a single command.

The docker-compose CLI utility allows users to run commands on multiple containers at once, for example, building images, scaling containers, running containers that were stopped, and more.

First, please download and install Docker Compose if you haven't already done so.

$ docker --version
Docker version 19.03.8, build afacb8b

$ docker-compose --version
docker-compose version 1.25.5, build 8a1c60f6

Config File Structure

Let's start with our config file structure, this can help you better understand the whole workflow:

├── compose
│   └── local
│       └── web
│           ├── Dockerfile
│           ├── entrypoint
│           └── start
├── docker-compose.yml
├── manage.py
├── wagtail_bootstrap_blog
└── requirements.txt

You will see we have config files docker-compose.yml and some files in compose directory, you do not need to create them for now. I will talk about them with more details in the coming sections.

Note: The config file structure come from cookiecutter-django, which is a great project for people who want to learn Django.

Compose file

Note: we can ignore the npm run start here and I will talk about it later.

Compose file is a YAML file to configure your application's services.

When you run docker-compose command, if you do not specify Compose file, the default file is docker-compose.yml, that is why we create docker-compose.yml at root directory of Django project, because it can save us time when typing command during development.

Let's add docker-compose.yml

version: "3.7"

services:
  web:
    build:
      context: .
      dockerfile: ./compose/local/web/Dockerfile
    image: wagtail_bootstrap_blog_web
    command: /start
    volumes:
      - .:/app
    ports:
      - 8000:8000
    env_file:
      - ./.env/.dev-sample
    depends_on:
      - db

  db:
    image: postgres:12.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_DB=wagtail_bootstrap_blog
      - POSTGRES_USER=wagtail_bootstrap_blog
      - POSTGRES_PASSWORD=wagtail_bootstrap_blog

volumes:
  postgres_data:

Notes:

  1. Here we defined two services, one is web (django devserver), the other one is db
  2. We create a named docker volume postgres_data, and use it to store the db data, so even db container is deleted, the db data can still exist.

Environment Variables

We can put env variables in a specific file for easy management.

Let's create .env directory, and add .dev-sample file

SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=wagtail_bootstrap_blog
SQL_USER=wagtail_bootstrap_blog
SQL_PASSWORD=wagtail_bootstrap_blog
SQL_HOST=db
SQL_PORT=5432

Please make sure .env is not excluded in the .gitignore, so it can be added to Git repo

Please note that the db login credential should match environment variables of db service in docker-compose.yml

Next, let's update DATABASES of wagtail_bootstrap_blog/settings/base.py to read env variables.

import os

DATABASES = {
    "default": {
        "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
        "NAME": os.environ.get("SQL_DATABASE", os.path.join(BASE_DIR, "db.sqlite3")),
        "USER": os.environ.get("SQL_USER", "user"),
        "PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
        "HOST": os.environ.get("SQL_HOST", "localhost"),
        "PORT": os.environ.get("SQL_PORT", "5432"),
    }
}

Dockerfile

In Docker Compose, we can let it create docker container from existing docker image or custom docker image.

To build custom docker image, we need to provide Dockerfile

Please create directory and file like this

├── compose
│   └── local
│       └── web
│           ├── Dockerfile

Edit compose/local/web/Dockerfile

FROM python:3.8-slim-buster

ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

# Install system packages required by Wagtail and Django.
RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \
    build-essential \
    libpq-dev \
    libmariadbclient-dev \
    libjpeg62-turbo-dev \
    zlib1g-dev \
    libwebp-dev \
 && rm -rf /var/lib/apt/lists/*

# Requirements are installed here to ensure they will be cached.
COPY ./requirements.txt /requirements.txt
RUN pip install -r /requirements.txt

COPY ./compose/local/web/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint

COPY ./compose/local/web/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start

WORKDIR /app

ENTRYPOINT ["/entrypoint"]

Notes:

  1. PYTHONDONTWRITEBYTECODE=1 tell Python to not write bytecode (.pyc) and __pycache__ directory on local env.
  2. RUN sed -i 's/\r$//g' /entrypoint is used to process the line endings of the shell scripts, which converts Windows line endings to UNIX line endings.
  3. In the above docker-compose.yml, we config docker volume .:/app, so here we set WORKDIR /app. If we edit code on host machine, then the code change can also been seen in /app of the docker container.

Next, let's check the entrypoint and start script.

Entrypoint

In docker-compose.yml, we can use depends_on to let web service run after db service. However, it can not guarantee web service start after db service is trully ready. (Github Issue)

So we can add script in entrypoint to solve this problem.

compose/local/web/entrypoint

#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

postgres_ready() {
python << END
import sys

import psycopg2

try:
    psycopg2.connect(
        dbname="${SQL_DATABASE}",
        user="${SQL_USER}",
        password="${SQL_PASSWORD}",
        host="${SQL_HOST}",
        port="${SQL_PORT}",
    )
except psycopg2.OperationalError:
    sys.exit(-1)
sys.exit(0)

END
}
until postgres_ready; do
  >&2 echo 'Waiting for PostgreSQL to become available...'
  sleep 1
done
>&2 echo 'PostgreSQL is available'

exec "[email protected]"
  1. We defined a postgres_ready function which is called in loop. The loop would only stop if the db service is able to connect.
  2. The last exec "[email protected]" is used to make the entrypoint a pass through to ensure that Docker container runs the command the user passes in (command: /start, in our case). For more, check this Stack Overflow answer.

Start script

Now, let's add start script.

compose/local/web/start

#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

python manage.py migrate
python manage.py runserver 0.0.0.0:8000

Start application

Let's update requirements.txt to include postgres dependency

Django>=3.1,<3.2
wagtail>=2.11,<2.12
psycopg2-binary

Building the docker images:

$ docker-compose build

Once the images are build, start the application in detached mode:

$ docker-compose up -d

# check realtime logs
$ docker-compose logs -f

web_1  | Django version 3.1.4, using settings 'wagtail_bootstrap_blog.settings.dev'
web_1  | Starting development server at http://0.0.0.0:8000/
web_1  | Quit the server with CONTROL-C.

This will start containers based on the order defined in the depends_on option. (db first, web second)

  1. Once the containers are up, the entrypoint scripts will execute.
  2. Once Postgres is up, the respective start scripts will execute. The Django migrations will be applied and the development server will run. The Django app should then be available.

You can check the docker compose application with this command.

$ docker-compose ps
        Name                       Command              State           Ports
--------------------------------------------------------------------------------------
wagtail_project_db_1    docker-entrypoint.sh postgres   Up      5432/tcp
wagtail_project_web_1   /entrypoint /start              Up      0.0.0.0:8000->8000/tcp

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, Wagtail CMS and React.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.


Table of Contents

This book will teach you how to build a modern blog with Wagtail CMS

Read More

Subscribe

Get notified about new great Web Development Tutorial