WordPress with PHP 7.4 and MySQL 8.0 using Docker Compose

Disclaimer: This post was not meant to be published. While I was doing WordPress with PHP 7.4 setup I thought someone might find it useful and decided to make it public. Only for testing / development purposes.

TL;DR: You can find source code in https://github.com/aurkenb/docker-wordpress-lemp

It’s been a while since I last posted to the public. So long that, to my shame, I just found out my self-hosted WordPress blog won’t upgrade anymore. It was left in the dark on some server untouched for years. So here I am again, willing to share some ideas. Considering whether to move to Medium in order to avoid blog maintenance or stick with well known WordPress.

Medium became very popular in the last few years. It’s a great tool for writing, sharing ideas and getting discovered. They’ve built-in audience which you cannot really match on your own. Like most (hosted) platforms though, it has a major drawback: you don’t own your distribution. You are not in control of the channel. You just rented out the space. And therefore you are subject to nasty things happening: censorship, changes in algorithms that may affect your visibility, control over your content, inability to reach your audience, or some out-of-your-control random action going on. 

Sahil Lavingia suffering some new functionality.

Don’t get me wrong, it’s perfectly fine to publish on Medium. It’s just that it doesn’t sound reasonable to build my content solely on them. Good old-fashioned blogging suits me best for my own publishing: my site, my brand, my content, my rules.

WordPress with PHP 7.4

It’s clear by now. I’m sticking with WordPress. Let’s try to avoid falling into past mistakes. The new blog installation must be easy setup, maintain, remove and re-create at will. Web pages are complex systems and many parts are involved. Each part needs its own setup. Years ago the process was long and painful. Luckily, the state of the art of the tools have dramatically improved over time. And there’s a new player in the room since I first installed my blog: it’s called Docker.

What’s Docker? AWS Docker page defines it as “a software platform that allows you to build, test, and deploy applications quickly. Docker packages software into standardized units called containers that have everything the software needs to run including libraries, system tools, code, and runtime.

In other words, Docker lets you create a “box” (container) with all you need to run your software. In our case, we will use Docker Compose, that runs multiple containers as a single service. The main advantage is you setup once and deploy as many times as you want. It will let you install and configure your self-hosted WordPress with a single click. Yes, you are right. You can re-install the whole blog with a single click once and again.

Having introduced a little bit of context, without further delay, let’s dig into the code.

Show me the code

We will start by creating a file named “.env”. This file will store all relevant variables used by the stack.

DOMAIN_NAME=localhost.example

NGINX_VERSION=latest
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
NGINX_LOG_DIR=./core/logs/nginx

WORDPRESS_VERSION=php7.4-fpm
WORDPRESS_DIR=./core/wordpress
WORDPRESS_THEME_DIR=
WORDPRESS_IMPORT_FILE=
WORDPRESS_DB_NAME=wordpress
WORDPRESS_DB_HOST=mysql
WORDPRESS_DB_USER=root
WORDPRESS_DB_PASSWORD=password
WORDPRESS_TABLE_PREFIX=wp_

MYSQL_VERSION=latest
MYSQL_ROOT_PASSWORD=password
MYSQL_USER=root
MYSQL_PASSWORD=password
MYSQL_DATABASE=wordpress
MYSQL_ALLOW_EMPTY_PASSWORD=no
MYSQL_DIR=./core/mysql

Pretty straightforward stuff.

We define a DOMAIN_NAME that will be URL of the installation. Ports 80/443 are used by default. Obviously changing NGINX_HTTP_PORT and NGINX_HTTPS_PORT values changes the URL to something like http(s)://DOMAIN_NAME:PORT/. We chose PHP 7.4 + MySQL 8.0 which take advantage of caching_sha2_password auth plugin instead of the old mysql_native_password.

Our project will consist of 3 containers:

  • Latest WordPress with PHP 7.4 as CMS.
  • MySQL 8.0 as database.
  • Nginx latest version as webserver.

Let’s define them “docker-compose.yml” file. 

Starting with WordPress …

wordpress:
    image: wordpress:${WORDPRESS_VERSION:-php7.4-fpm}
    container_name: wordpress
    depends_on:
      - mysql
    environment:
      - WORDPRESS_TABLE_PREFIX=${WORDPRESS_TABLE_PREFIX:-wp_}
      - WORDPRESS_DB_NAME=${WORDPRESS_DB_NAME:-wordpress}      
      - WORDPRESS_DB_HOST=${WORDPRESS_DB_HOST:-mysql}
      - WORDPRESS_DB_USER=${WORDPRESS_DB_USER:-wordpress}
      - WORDPRESS_DB_PASSWORD=${WORDPRESS_DB_PASSWORD:-password}
    volumes:
      - ${WORDPRESS_DIR:-./core/wordpress}:/var/www/html
    restart: always  
    networks:
      - wordpress-network

Things start to get interesting here. We tell Docker to use latest WordPress version image with PHP 7.4 and set environment variables to the ones adjusted in .env file. If they are missing, we use defaults. Volumes: section sets where WordPress will be placed.

mysql:
    image: mysql:${MYSQL_VERSION:-latest}
    container_name: mysql
    volumes:
      - ${MYSQL_DIR:-./core/mysql}:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-password}
      - MYSQL_DATABASE=${MYSQL_DATABASE:-wordpress}
      - MYSQL_USER=${MYSQL_USER:-root}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD:-password}
      - MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-no}
    restart: always
    networks:
      - wordpress-network

MySQL part is no different from PHP. We define working directory and some MySQL variables.

nginx:
    build: 
      context: ./config/nginx
      dockerfile: Dockerfile
      args:
        - NGINX_IMAGE=nginx:${NGINX_VERSION:-latest}
        - DOMAIN_NAME=${DOMAIN_NAME:-localhost}
    container_name: nginx
    depends_on:
      - wordpress
    ports:
      - '${NGINX_HTTP_PORT:-80}:80'
      - '${NGINX_HTTPS_PORT:-443}:443'
    volumes:
      - ${WORDPRESS_DIR:-./core/wordpress}:/var/www/html
      - ${NGINX_LOG_DIR:-./core/logs/nginx}:/var/log/nginx
    restart: always
    networks:
      - wordpress-network

Nginx section uses a slight variation. It’s not required but adds a little extra flexibility to the build. Instead of choosing an image, we build our own passing two variables (NGINX_IMAGE + DOMAIN_NAME) and adding some magic. A file named “Dockerfile” is expected inside config/nginx/ folder. It will hold instructions to create the container.

Also defines ports:to redirect ports to the ones adjusted in .env. Volumes section stays the same, nothing new.

Create config folder and another folder nginx inside it. Create two files there: Dockerfile and default.conf.

Copy and paste the following into Dockerfile file:

ARG NGINX_IMAGE
FROM $NGINX_IMAGE

ARG DOMAIN_NAME
COPY ./default.conf /etc/nginx/conf.d/default.conf
RUN sed -i -r "s/localhost/${DOMAIN_NAME}/g" /etc/nginx/conf.d/default.conf

It will tell Docker which image to select $NGINX_IMAGE and copy default configuration to be used by Nginx. Additionally sets Nginx server_name to DOMAIN_NAME (remember .env file).

Copy and paste in default.conf:

server {
    listen      80;
    listen [::]:80;
    server_name localhost;

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

    server_tokens off;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

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

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass wordpress:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

The full docker-compose.yml file looks like this:

version: '3'
services:

  wordpress:
    image: wordpress:${WORDPRESS_VERSION:-php7.4-fpm}
    container_name: wordpress
    depends_on:
      - mysql
    environment:
      - WORDPRESS_TABLE_PREFIX=${WORDPRESS_TABLE_PREFIX:-wp_}
      - WORDPRESS_DB_NAME=${WORDPRESS_DB_NAME:-wordpress}      
      - WORDPRESS_DB_HOST=${WORDPRESS_DB_HOST:-mysql}
      - WORDPRESS_DB_USER=${WORDPRESS_DB_USER:-wordpress}
      - WORDPRESS_DB_PASSWORD=${WORDPRESS_DB_PASSWORD:-password}
    volumes:
      - ${WORDPRESS_DIR:-./core/wordpress}:/var/www/html
    restart: always  
    networks:
      - wordpress-network
      
  mysql:
    image: mysql:${MYSQL_VERSION:-latest}
    container_name: mysql
    volumes:
      - ${MYSQL_DIR:-./core/mysql}:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-password}
      - MYSQL_DATABASE=${MYSQL_DATABASE:-wordpress}
      - MYSQL_USER=${MYSQL_USER:-root}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD:-password}
      - MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-no}
    restart: always
    networks:
      - wordpress-network

  nginx:
    build: 
      context: ./config/nginx
      dockerfile: Dockerfile
      args:
        - NGINX_IMAGE=nginx:${NGINX_VERSION:-latest}
        - DOMAIN_NAME=${DOMAIN_NAME:-localhost}
    container_name: nginx
    depends_on:
      - wordpress
    ports:
      - '${NGINX_HTTP_PORT:-80}:80'
      - '${NGINX_HTTPS_PORT:-443}:443'
    volumes:
      - ${WORDPRESS_DIR:-./core/wordpress}:/var/www/html
      - ${NGINX_LOG_DIR:-./core/logs/nginx}:/var/log/nginx
    restart: always
    networks:
      - wordpress-network
      
volumes:
  mysql: {}
  wordpress: {}      

networks:
  wordpress-network:
    driver: bridge

You can now run:

docker-compose up -d --build

Here we are

You are ready to go! Up and running latest version of WordPress with PHP 7.4 and Nginx successfully installed.

Complete source code is on Github.

Do you have questions? Suggestions? Feel free to drop me a comment!

Thanks! I promise to only send relevant stuff to your inbox.

Join the conversation

2 Comments

  1. Great tutorial! I learned how to use .env files in docker. I have no idea before this. Thanks!

    I have an issue with WordPress. In site Health Status:

    The REST API is one way WordPress, and other applications, communicate with the server. One example is the block editor screen, which relies on this to display, and save, your posts and pages.

    The REST API request failed due to an error.
    Error: cURL error 28: Resolving timed out after 10000 milliseconds (http_request_failed)

    Any idea of why this happens?

    1. Thanks Deryck!

      There’s more than a reason for this happening. From the message “cURL error 28:” here I am inclined to think your firewall is blocking outbound traffic to port 80. Considering you are using i.e UFW, you will need to run:
      ufw allow out 80 && ufw reload.
      Bear in mind that opening outbound traffic is NOT a trivial decision as you open up the door to reverse shell attacks.

      Hope it helps!

Leave a comment

Your email address will not be published. Required fields are marked *