Complete Guide: Deploying Wagtail CMS to Production with Docker, Nginx, and Let's Encrypt

I followed these steps to create and deploy defjoy.site

Deploying a Wagtail CMS application to production can seem daunting, but with the right tools and configuration, it becomes a straightforward process. In this comprehensive guide, I'll walk you through deploying a Wagtail application using Docker, Nginx, Gunicorn, PostgreSQL, and securing it with Let's Encrypt SSL certificates.

What We'll Build

By the end of this tutorial, you'll have:

  • A production-ready Wagtail CMS running in Docker containers
  • PostgreSQL database for persistent storage
  • Nginx as a reverse proxy serving static files
  • SSL/HTTPS enabled with Let's Encrypt certificates
  • Automatic certificate renewal

Prerequisites

Before we begin, you'll need:

  • A DigitalOcean Ubuntu 24.04 LTS server (or similar VPS)
  • A domain name with DNS access
  • Basic familiarity with Linux command line
  • SSH access to your server

Step 1: DNS Configuration

First, configure your domain's DNS records. In your domain registrar's DNS settings (Namecheap, GoDaddy, etc.), add these records:

Host Type Value
@ A Record YOUR_SERVER_IP
www CNAME yourdomain.com

Note: DNS propagation can take up to 48 hours, but usually completes within a few hours.


Step 2: Setting Up SSH Keys

SSH keys provide secure authentication for both GitHub and your server.

Generate a new SSH key

ssh-keygen -t ed25519 -C "your_email@example.com"

Add the key to ssh-agent

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

Add to GitHub

Display your public key:

cat ~/.ssh/id_ed25519.pub

Copy the output and paste it into GitHub → Settings → SSH and GPG Keys → New SSH Key.

Test the connection

ssh -T git@github.com

You should see a success message confirming authentication.


Step 3: Installing Docker and Docker Compose

Docker will containerize our application, making deployment consistent and reproducible.

Update system packages

sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings

Add Docker's official GPG key

curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Add Docker repository

echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER

Important: Log out and back in (or reboot) for group changes to take effect.


Step 4: PostgreSQL Database Setup

PostgreSQL will serve as our production database.

Install PostgreSQL

sudo apt install -y python3-dev libpq-dev postgresql postgresql-contrib

Create database and user

Switch to the postgres user and access the PostgreSQL prompt:

sudo su - postgres
psql

Run these SQL commands:

CREATE DATABASE your_database_name;
CREATE USER your_db_user WITH PASSWORD 'your_secure_password';
ALTER ROLE your_db_user SET client_encoding TO 'utf8';
ALTER ROLE your_db_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE your_db_user SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE your_database_name TO your_db_user;

Grant schema permissions:

sudo -u postgres psql your_database_name
GRANT ALL ON SCHEMA public TO your_db_user;
ALTER SCHEMA public OWNER TO your_db_user;
GRANT CREATE, USAGE ON SCHEMA public TO your_db_user;

Exit the PostgreSQL prompt:

\q
exit

Configure PostgreSQL for Docker access

Edit the PostgreSQL configuration file:

sudo nano /etc/postgresql/16/main/postgresql.conf

Find and set:

listen_addresses = '*'

Edit the host-based authentication file:

sudo nano /etc/postgresql/16/main/pg_hba.conf

Add these lines at the end:

host all all 172.17.0.0/16 md5
host all all 172.18.0.0/16 md5

Restart PostgreSQL:

sudo systemctl restart postgresql

Verify PostgreSQL is listening

ss -tln | grep 5432

You should see 0.0.0.0:5432 in the output.


Step 5: Clone Your Project

Create the project directory and clone your repository:

sudo mkdir -p /opt/your-project-name
sudo chown -R $USER:$USER /opt/your-project-name
cd /opt/your-project-name
git clone git@github.com:yourusername/your-repo.git .

Step 6: Environment Configuration

Create a .env file in your project root:

nano .env

Add your environment variables:

DJANGO_SECRET_KEY=your-secret-key-here
DJANGO_SETTINGS_MODULE=your_project.settings.production

DB_NAME=your_database_name
DB_USER=your_db_user
DB_PASSWORD=your_secure_password
DB_HOST=host.docker.internal
DB_PORT=5432

Security tip: Generate a strong secret key using python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'


Step 7: Dockerfile Configuration

Create a Dockerfile in your project root:

FROM python:3.12-slim-bookworm

RUN useradd wagtail
ENV PYTHONUNBUFFERED=1
ENV PORT=8000

RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \
    build-essential libpq-dev libjpeg62-turbo-dev zlib1g-dev libwebp-dev \
    && rm -rf /var/lib/apt/lists/*

RUN pip install "gunicorn==20.0.4"

COPY requirements.txt /requirements.txt
RUN pip install -r /requirements.txt

WORKDIR /app
RUN chown -R wagtail:wagtail /app

COPY --chown=wagtail:wagtail . .

USER wagtail
EXPOSE 8000

CMD set -xe; \
    python manage.py migrate --noinput; \
    gunicorn your_project.wsgi:application --bind 0.0.0.0:8000 --workers 3

Step 8: Docker Compose Setup

Create docker-compose.yml:

services:
  web:
    build: .
    restart: always
    env_file:
      - .env
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      - "8000:8000"

Build and run containers

docker compose build
docker compose up -d

Step 9: Static Files Management

Fix permissions for static and media directories:

sudo chown -R $USER:$USER staticfiles
sudo chown -R $USER:$USER media

Collect static files:

docker compose exec web python manage.py collectstatic --noinput

Step 10: Nginx Reverse Proxy Configuration

Install Nginx if not already installed:

sudo apt install -y nginx

Create the Nginx configuration file:

sudo nano /etc/nginx/sites-available/your_project.conf

Add this configuration:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    location /static/ {
        alias /opt/your-project-name/staticfiles/;
    }

    location /media/ {
        alias /opt/your-project-name/media/;
    }

    client_max_body_size 20M;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable the site and restart Nginx:

sudo ln -s /etc/nginx/sites-available/your_project.conf \
          /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Step 11: SSL Certificate with Let's Encrypt

Install Certbot:

sudo apt install -y certbot python3-certbot-nginx

Once your site is accessible via HTTP, obtain an SSL certificate:

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Follow the prompts to complete the setup.

Test automatic renewal

sudo certbot renew --dry-run

Step 12: Django Production Security Settings

Add these settings to your production.py settings file:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Restart the Docker container:

docker compose restart web

Step 13: Verification

Test your application

curl -I https://yourdomain.com

Test static file serving

curl -I https://yourdomain.com/static/css/core.css

Both should return:

HTTP/1.1 200 OK

Troubleshooting Tips

Container logs

View logs if something isn't working:

docker compose logs -f web

Nginx logs

Check Nginx error logs:

sudo tail -f /var/log/nginx/error.log

Database connection issues

Test database connectivity from inside the container:

docker compose exec web python manage.py dbshell

Static files not loading

Ensure permissions are correct and paths match in your Nginx config and Django settings.


Conclusion

Congratulations! You now have a production-ready Wagtail CMS deployment with:

  • ✅ Containerized application using Docker
  • ✅ PostgreSQL database
  • ✅ Nginx reverse proxy
  • ✅ SSL/HTTPS encryption
  • ✅ Automatic certificate renewal