I’m not a very big fan of WordPress. IMO main problem (and benefit?) is that WordPress has many plugins. So you can find plugins for almost everything. But here are plugins in different quality and sometimes just one plugin on your page blocking you from upgrade to a new version because the developer not updating it anymore. On the other side is easy to set up a new page and start posting. You need to upload the page over FTP, create the database and run the installer. (I did this procedure very long ago.)

Nowadays when you can use composer to manage dependencies and deploy PHP apps with CI/CD it’s a step back IMO… Maybe it’s because I’m lazy but even with WordPress you can forget about uploading sites over FTP, and you can use Docker Image to deploy the page.

So with docker you can do just with one command:

  • Deploy wordpress site
  • Deploy and configure databse
  • Generate SSL certificate.

What you will need?

  • Ovn server (or VPS) with installed docker & docker-compose.
  • Own web address
  • Configured DNS server which point address to your server

If you want host from home you have to have static external IP address, and properly setup NAT on your router. Another way but more advanced is create VPS with some cloud provider and create VPN network, then you can forward requests to your server from proxy to home server over VPN. Ok Back to docker…

If you dont have installed docker and/or docker-compose you can find it Here for Docker and here is docker-compose.

At first Networks:

Let’s start by creating docker-compose file:

version: "3.9"

networks:
  frontend:
    external: true
  wordpress_backend:
    internal: true

We will have two networks:

  • frontend: this will be used to comunicate from internet with our application
  • wordpress_backend: network for communicate between wprdpress, db and nginx… It is not awailable from internet.

External networks are not created automaticaly with docker compose so create it manually:

docker network create frontend

Services

version: "3.9"

networks:
  frontend:
    external: true
  wordpress_backend:
    internal: true

services:
  # here goes services

Traefik

At first, add Traefik service. This is our reverse proxy. It allows connecting to our app, upgrading HTTP to HTTPS and it can request SSL certificates for services behind him.

wordpress_traefik:
  image: traefik
  command:
    --configFile=/traefik.yml
  restart: unless-stopped
  networks:
    - frontend
    - wordpress_backend
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - /etc/localtime:/etc/localtime:ro
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - ./traefik.yml:/traefik.yml:ro
    - ./tls_config.yml:/tls_config.yml:ro
    - ./letsencrypt:/letsencrypt
  environment:
    - CF_API_EMAIL=<your-email>
    - CF_API_KEY=<your-cloudflare-api-key>

I’m using DNS challenge to renew and generate certificates but there are more Supported provider.

Configure traefik:

# traefik.yml
api:
  dashboard: true
  insecure: true

log:
  level: "DEBUG"

providers:
  docker:
    exposedbydefault: false
  
  file:
    filename: "/tls_config.yml"
    watch: true

entrypoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesresolvers:
  le:
    acme:
      httpchallenge:
        entrypoint: "web"
      emaiL: "<change to your email>"
      storage: "/letsencrypt/acme.json"
      dnschallenge:
        provider: "cloudflare"

accesslog: true

serversTransport:
  insecureSkipVerify: true

Disable TLSv1: by default traefik has enabled it.

# tls_config.yml
tls:
  options:
    mytls:
      sniStrict: true
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
      curvePreferences:
        - CurveP521
        - CurveP384
    mintls13:
      minVersion: VersionTLS13

With this config you can ged “A+” rank for your server with SSL Labs test

Wordpress Web

I using an FPM image for WordPress so I need NGINX. Add to your services, and configure traefik with labels.

wordpress_proxy:
  image: nginx:alpine
  restart: unless-stopped
  depends_on:
    - wordpress_app
    - wordpress_db
  volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf:ro
  volumes_from:
    - wordpress_app
    - wordpress_traefik
  networks:
    - wordpress_backend
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.wordpress.entrypoints=web,websecure"
    - "traefik.http.routers.wordpress.rule=Host(`<change to your web address>`)"
    - "traefik.http.routers.wordpress.tls.certresolver=le"
    - "traefik.http.routers.wordpress.tls.options=mytls@file"
    - "traefik.http.routers.wordpress.service=wordpress"
    - "traefik.http.services.wordpress.loadbalancer.server.port=80"

And configure Nginx:

# nginx.conf
user nginx;

events {
  worker_connections 768;
}

http {
  upstream backend {
    server wordpress_app:9000;
  }

  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  gzip on;
  gzip_disable "msie6";

  server {
    listen 80;

    root /var/www/html/;
    index index.php index.html index.htm;

    location / {
      # try_files $uri $uri/ =404;
      try_files $uri $uri/ /index.php?$args;
    }

    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
      root /usr/share/nginx/html;
    }

    location = /favicon.ico {
      log_not_found off;
      access_log off;
    }

    location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
      access_log off; log_not_found off; expires max;
    }


    location ~ [^/]\.php(/|$) {
      fastcgi_split_path_info ^(.+?\.php)(/.*)$;
      if (!-f $document_root$fastcgi_script_name) {
        return 404;
      }
      # This is a robust solution for path info security issue and works with "cgi.fix_pathinfo = 1" in /etc/php.ini (default)

      include fastcgi_params;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PHP_VALUE "upload_max_filesize=64m
      post_max_size=64m";
      fastcgi_pass wordpress_app:9000;
    }
  }
}

Wordpress

Add WordPress service. I am using the FPM version. WordPress will need to access to the internet so add it to both networks frontend for internet access and wordpress_backend for the ability to connect to the database. We don’t need traefik here so disable it by add - "traefik.enable=false" to labels.

wordpress_app:
  image:  wordpress:5-php8.0-fpm-alpine # wordpress:5-fpm-alpine
  restart: unless-stopped
  depends_on:
    - wordpress_db
  networks:
    - wordpress_backend
    - frontend
  environment:
    WORDPRESS_DB_HOST: wordpress_db
    WORDPRESS_DB_USER: exampleuser
    WORDPRESS_DB_PASSWORD: examplepass
    WORDPRESS_DB_NAME: exampledb
  volumes:
    - wordpress:/var/www/html
  labels:
    - "traefik.enable=false"

Database

WordPress needs to MySQL database to run. So add it (MariaDB is also supported). Environment Variables must correspond with the WordPress DB variable. WordPress will create all database tables at the first run.

wordpress_db:
  image: mysql:8.0.21
  restart: unless-stopped
  command: --default-authentication-plugin=mysql_native_password
  networks:
    - wordpress_backend
  environment:
    MYSQL_DATABASE: exampledb
    MYSQL_USER: exampleuser
    MYSQL_PASSWORD: examplepass
    MYSQL_RANDOM_ROOT_PASSWORD: '1'
  volumes:
    - db:/var/lib/mysql

Adminer

This is mostly for development when you need to look inside the database.

adminer:
  image: adminer
  depends_on:
    - wordpress_db
  networks:
    - frontend
    - wordpress_backend
  restart: always

Volumes

Volumes to store your data. You can map host folders instead of volumes, then you don’t need the following lines of code. Your docker-compose file wi have the following structure.

version: "3.9"

networks:
  # your networks

services:
  # your services

# Add following content at the end
volumes:
  wordpress:
    driver: local
  db:
    driver: local

Folder sturcture

Your folder has to contain the following files

docker-compose.yml
nginx.conf
tls_config.yml
traefik.yml

Start it

Ok let’s start our Wordpress

docker-compose up -d

And navigate to your address. WordPress will ask you to create a new admin user and password.

Delete it

If you need to delete it you can to do this with

docker-compose down -v

But warning: This also delete volumes with all of its data In production, I recommend using host folders mapping instead of volumes which docker-compose down -v not deleting.

Updating

To update the container you can run

docker-compose pull
docker-compose up -d

Which will download newer docker images and recreate containers.

Done

And that’s your very own WordPress running in docker containers. If you have more questions you can contact me over email, my Mastodon or Matrix account.