Rendition object (181)

Deploy Your Wagtail Webpage with Docker, PostgreSQL, Gunicorn & NGINX

Want to publish your Wagtail site in a Docker environment with PostgreSQL, Gunicorn and NGINX on a production server? The following tutorial will show you an easy way.

Prerequisite:
This tutorial is based on the previous article: Dockerize Wagtail & PostgreSQL as a development environment, which you should have read. To follow this tutorial it is necessary to have a working Docker environment including docker-compose installed on your development machine and on the production server and a fresh Wagtail project as described in the article.

Install Gunicorn

This is particularly simple. The following entry in requirements.txt installs Gunicorn when starting the environment.

gunicorn==20.0.4

Build the Docker production environment

To do this, we need to add the following files to our project:

.env.prod
.env.prod.db
docker-compose.prod.yml
app/entrypoint.prod.sh
app/Dockerfile.prod

Thus, the structure of the overall project should be as follows:

.
├── app
│   ├── app
│   ├── home
│   ├── search
│   ├── Dockerfile
│   ├── Dockerfile.prod
│   ├── entrypoint.prod.sh
│   ├── entrypoint.sh
│   ├── manage.py
│   └── requirements.txt
├── .env.dev
├── .env.dev.db
├── .env.prod
├── .env.prod.db
├── .gitignore
├── README.md
├── docker-compose.prod.yml
└── docker-compose.yml

The environment variables for web and database

Create .env.prod and .env.prod.db at the root level of your project and customize the information according to your production environment.

# .env.prod
DEBUG=False
SECRET_KEY=YOUR DJANGO SECRET KEY
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 your-domain.com [::1]

SQL_ENGINE=django.db.backends.postgresql_psycopg2
SQL_DATABASE=demo_wagtail_prod
SQL_USER=demouserprod
SQL_PASSWORD=DemoPass
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres


# .env.prod.db
POSTGRES_USER=demouserprod
POSTGRES_PASSWORD=DemoPass
POSTGRES_DB=demo_wagtail_prod

We need to adjust the Wagtail settings files according to the environment variables. For this we edit the settings/production.py as follows:

# app/app/settings/production.py
from .base import *

DJANGO_ROOT = '/home/app/'

SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = os.environ.get("DEBUG")
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")

# Database PostgreSQL
DATABASES = {
    'default': {
        'ENGINE': os.environ.get("SQL_ENGINE"),
        'NAME': os.environ.get("SQL_DATABASE"),
        'USER': os.environ.get("SQL_USER"),
        'PASSWORD': os.environ.get("SQL_PASSWORD"),
        'HOST': os.environ.get("SQL_HOST"),
        'PORT': os.environ.get("SQL_PORT"),
    }
}

try:
    from .local import *
except ImportError:
    pass

We also need an entrypoint sh-script for the production system that checks if the database has been started successfully and then performs some Django/Wagtail tasks (/app/entrypoint.prod.sh).

#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

python manage.py makemigrations --settings=app.settings.production
python manage.py migrate --settings=app.settings.production
python manage.py collectstatic --settings=app.settings.production --no-input --clear
python manage.py update_index --settings=app.settings.production
exec "$@"

Dockerize for production

To create the two Docker instances for our production environment, we still need the corresponding docker-compose and Docker files (app/Dockerfile.prod and docker-compose.prod.yml).

To allow NGINX to access the /media and /static directories later, we mount the volumes /var/www/app/static and /var/www/app/media on the server.

# app/Dockerfile.prod

# Pull the base image
FROM python:3.8.5-alpine3.12

# Create app directory
RUN mkdir -p /home/app

# Create app user
RUN addgroup -S app && adduser -S app -G app

# Create directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/static
RUN mkdir $APP_HOME/media
WORKDIR $APP_HOME

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Install server packages
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev libffi-dev openssl-dev \
    && apk add jpeg-dev libwebp-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev libxml2-dev libxslt-dev libxml2

# Install Python packages
RUN pip install pip --upgrade
COPY ./requirements.txt $APP_HOME
RUN pip install -r requirements.txt

# Copy production entrypoint
COPY entrypoint.prod.sh $APP_HOME

# Copy project
COPY . $APP_HOME

# Chown all files
RUN chown -R app:app $APP_HOME

# Change user to app
USER app

# Run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
# docker-compose.prod.yml
version: '3.7'

services:
  web:
    restart: always
    build:
      context: app
      dockerfile: Dockerfile.prod
    command: gunicorn app.wsgi:application --bind 0.0.0.0:8009
    volumes:
      - /var/www/app/static:/home/app/web/static
      - /var/www/app/media:/home/app/web/media
    ports:
      - 8009:8009
    env_file:
      - .env.prod
    depends_on:
      - db
  db:
    restart: always
    image: postgres:12.2-alpine
    volumes:
      - postgres_data_prod:/var/lib/postgresql/data/
    env_file:
      - .env.prod.db

volumes:
  postgres_data_prod:

Now we have created all the necessary files and we only have to add the production settings into app/app/wsgi.py

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings.production")

Build the Docker environment with:
docker-compose -f docker-compose.prod.yml build

Run the production with:
docker-compose -f docker-compose.prod.yml up -d

If everything went OK, you can test the new environment under http://127.0.0.1:8009

NGINX configuration

As a last step, we need to create a virtual host file for NGINX which acts as a load balancer for Gunicorn.

server {
    client_max_body_size 100M;
    listen 80;
    server_name www.yourdomain.com;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_pass http://127.0.0.1:8009;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        alias /var/www/app/static/;
    }
    location /media/ {
        alias /var/www/app/media/;
    }
}

And that's it. In just a few steps, we have a running Wagtail production system.

You can find the whole project ready to use on my GitHub repository:
phookycom / wagtailondocker