Deploying a WordPress Application to Production with Docker Compose

Production Deployment

Local docker-compose up is not enough.

Production deployment means:

  • Publicly accessible application
  • HTTPS
  • Obtaining and automatic renewal of certificates
  • Separated services
  • Backups
  • Monitoring
  • Logging
  • Security

What is a Reverse Proxy?

A reverse proxy is a server that:

  • accepts requests from the internet
  • forwards them to internal services
  • returns the response to the client

Why a Reverse Proxy?

  • Terminates HTTPS
  • HTTP → HTTPS redirection
  • Load balancing
  • Rate limiting
  • Security headers

What is Load Balancing?

Load balancing is:

  • Distributing incoming requests across multiple backend servers.

The goal is to:

  • increase availability (High Availability)
  • improve performance
  • eliminate single point of failure
  • enable horizontal scaling

Why Use Nginx?

  • high performance (event-driven architecture)
  • low memory consumption
  • stability
  • simple configuration
  • HTTPS support

Alternative – Traefik

Traefik:

  • automatically issues Let's Encrypt certificates
  • does not require a separate certbot
  • auto service discovery
  • simpler configuration

Suitable for:

  • multiple websites on one server
  • dynamic environments

Digital Certificate

HTTPS = HTTP + TLS Encryption

HTTPS ensures:

  • encrypted communication
  • data integrity
  • server identity verification

A Digital Certificate Contains:

  • domain (e.g. example.com)
  • public key
  • issuer (CA – Certificate Authority)
  • validity period of 90 days
  • CA digital signature

Why Is It Mandatory in Production?

  • browser security policy
  • protection of user data, cookies and sessions
  • SEO (Google favours HTTPS)

Who Issues Certificates?

  • Let's Encrypt
  • DigiCert
  • GlobalSign
  • Sectigo

Let's Encrypt

Let's Encrypt is:

a free, automated Certificate Authority (CA)

Domain Verification (Challenge)

Most common:

  • HTTP-01 (via web server)
  • DNS-01 (via DNS record)

What is Certbot?

a tool for obtaining and renewing certificates from Let's Encrypt.

  • requests a certificate
  • verifies domain ownership
  • stores the certificate
  • renews it automatically

Example Production Configuration

services:
                          INTERNET
                              │  HTTPS (443)
                              ▼
                     ┌───────────────────┐
                     │       NGINX       │
                     │  Reverse Proxy    │
                     │  SSL Termination  │
                     └─────────┬─────────┘
                               │  internal network
                               ▼
                     ┌───────────────────┐
                     │    WORDPRESS      │
                     │    (php-fpm)      │
                     └─────────┬─────────┘
                               │  internal network
                               ▼
                     ┌───────────────────┐
                     │      MARIADB      │
                     │    Database       │
                     └───────────────────┘
  wordpress:
    image: wordpress:php8.2-fpm
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: ${MYSQL_USER}
      WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
      WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
    volumes:
      - wp_data:/var/www/html
    depends_on:
      - db
  db:
    image: mariadb:10.6
    restart: always
    env_file: .env
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
  nginx:
    image: nginx:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - wp_data:/var/www/html
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./certbot/www:/var/www/certbot
      - certbot_conf:/etc/letsencrypt
    depends_on:
      - wordpress
    networks:
      - internal
      - public
  certbot:
    image: certbot/certbot:latest
    volumes:
      - certbot_conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done"
volumes:
  db_data:
  wp_data:
  certbot_conf:

networks:
  internal:
  public:

Nginx Configuration

nginx/conf.d/wordpress.conf

server {
    listen 80;
    server_name example.com www.example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    root /var/www/html;
    index index.php;
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        fastcgi_pass wordpress:9000;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

Let's Encrypt + Certbot

Issuing a Certificate

Run once:

docker run --rm \
  -v $(pwd)/certbot/conf:/etc/letsencrypt \
  -v $(pwd)/certbot/www:/var/www/certbot \
  certbot/certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  -d example.com -d www.example.com \
  --email admin@example.com \
  --agree-tos

Automatic Renewal

Cron job (saved to /etc/cron.d/certbot-renew):

0 3 * * * root \
  /usr/bin/docker run --rm \
  -v /path/certbot/conf:/etc/letsencrypt \
  -v /path/certbot/www:/var/www/certbot \
  certbot/certbot renew --quiet && \
  /usr/bin/docker compose -f /path/docker-compose.yml restart nginx

Monitoring and Tracking Metrics

What is Monitoring?

Continuous observation of the state of an application, infrastructure, and services.

Goal:

  • quickly detect problems
  • minimize downtime
  • prevent outages

Without monitoring we don't know that the disk is full, that a certificate has expired, that the database has crashed, that the web is returning 500 errors ...

What Do We Need to Monitor?

  • HW: CPU, RAM, Disk
  • SW: Docker (container state, restarts)
  • Application: HTTP 5xx errors, response time, availability (uptime)

What is Grafana?

Grafana is:

  • A visualization and alerting tool for metrics and logs.

Used for:

  • building dashboards
  • visualizing data from Prometheus, Loki, Elasticsearch…
  • setting up alerts
  • monitoring the state of applications in real time

What is Prometheus?

  • An open-source system for collecting, storing, and evaluating metrics.
  • Exporters expose metrics via an HTTP endpoint (/metrics)
  • Prometheus scrapes them at regular intervals
  • Stores them in its own time-series database (time-series DB)

What is a Metric?

Each metric has:

  • a name
  • a value
  • a timestamp
  • labels (e.g. container="wordpress")

Tracking Metrics

Prometheus has its own language: the Query Language – PromQL

Example:

  • calculating the number of HTTP requests in the last 5 minutes
  • filtering by labels
  • calculating percentages, averages, percentiles

Monitoring WordPress with Grafana

Goal: monitor the state of the server, Docker containers and the WordPress application.

Stack:

  • Prometheus – collects metrics
  • Grafana – visualizes metrics
  • node-exporter – server metrics (CPU, RAM, disk)
  • cAdvisor – Docker container metrics

Extending docker-compose.yml with Monitoring

  node-exporter:
    image: prom/node-exporter:latest
    restart: always
    pid: host
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    restart: always
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
  prometheus:
    image: prom/prometheus:latest
    restart: always
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
  grafana:
    image: grafana/grafana:latest
    restart: always
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - prometheus

Backups

The goal is protection against:

  • hardware failure
  • configuration error
  • cyber attack
  • human error

Backup strategies:

  • "hot backup": less secure, fast to perform and restore (e.g. copying to an attached disk)
  • "cold backup": more secure and slower. E.g. (copying to tape or CD and storing the media in a safe)

Database Backup

docker compose exec db \
  mysqldump -u root -p wordpress > backup.sql

Automation using a backup container:

backup:
  image: mariadb:10.6
  env_file: .env
  volumes:
    - ./backups:/backups
  networks:
    - internal
  entrypoint: >
    sh -c "mysqldump -h db -u root -p$$MYSQL_ROOT_PASSWORD $$MYSQL_DATABASE
    > /backups/backup_$$(date +%F).sql"
  depends_on:
    - db

Conclusion

Deploying WordPress to production is not just:

docker compose up -d

It is:

  • architecture
  • security
  • backups
  • monitoring
  • automation
  • operations
Reload?