Update – 2025-09-27!!

We have updated the docker-compose.yml example, MAC and IPv6, healthchecks, dependencies working off healthchecks, and better naming and MACVLAN pattern use.

More about that Update

Modern Docker Compose does not use the version tag anymore. Further, we included an example of the IPv6 and MAC Address assignment to the Docker Container as that allows the IPv6 address that would be rather mobile and shifting around to be settled and DNS locally to be more consistent. We also updated the MACVLAN Build script and the Docker Deploy script as well.


Stack consists of WordPress (PHP-FPM) / Nginx / Mysql / WPCLI.

Again, I will eventually get around to a longer opening, but till then here we go.

Expectations are you have a Docker Host already, can install and run “Hello World” via docker-compose. If you can’t do that, you won’t be able to do this. Sorry I don’t have a guide to suggest yet, but, the creators of docker-compose have done a good job at making it approachable. Generally get Python 3, and docker installed, then follow their guide for docker-compose.

When I do one of these, either I run it on my Docker Host from the Git repo I track my code in, or, well, that is about how I do it now. That said, I have a folder for each “project” or Stack/Container purpose. I navigate to the folder that has the docker-compose.yml and various other bits, and I run my “run” script that is written to execute (generally ‘docker-compose up -d …’) the run environment details (build/execute/update) for that project.

WordPress in Docker requires a DB, for this I choose MySQL because the Maria DB docker container doesn’t natively support the (older) method of authentication that Docker uses as it fabricates your container. That said, once they weave that into the container rollout, we will move over to MariaDB.

cat deploy_container.sh
#!/bin/bash
set -eo pipefail
# Get the name of the current folder (stack)
STACK_NAME=$(basename "$(pwd)")
echo "Starting image update check for stack ${STACK_NAME}..."

# Function to implement retry logic
retry_logic() {
    local max_attempts=$1
    shift
    local cmd=$@
    max_attempts_text=$((max_attempts - 1))
    for ((i=1; i<max_attempts; i++)); do
        echo "Attempt ${i}/${max_attempts_text}: "
        if eval "$cmd"; then
            return 0
        else
            echo "Failure. Retrying..."
            sleep 5
        fi
    done
    
    echo "Failed after ${max_attempts_text} attempts."
    exit 1
}

# Run docker compose with retry logic
retry_logic 4 "docker compose pull"

docker compose up -d
docker image prune -f

There’s the run script, in this we tell ‘docker-compose’ to do a number of things, but we don’t tell it to make my “macvlan” driver type “core01” network. This network uses a /26 of my whole network, and allows me to carve out IPs to specific services that aren’t being channeled through the Docker hosts’ network IP.

To create a “core01” network (you don’t have to do this, please read all the rest, get to the end where we talk about how to do ‘frontend’ instead of this ‘core01’ network.

cat build_core01.sh
#!/bin/bash
# dockerhostname
docker network create \
    --driver=macvlan \
    --subnet=192.168.34.64/26 --gateway=192.168.34.65 --ip-range=192.168.34.80/28 \
    --ipv6 --subnet=1000:2000:3000:4000::/64 --gateway=1000:2000:3000:4000:0123:4567:89ab:cdef \
    --attachable \
    -o parent=eth0 -o macvlan_mode=bridge \
    -o com.docker.network.mtu=1500 \
    core01

Now that we have a way to attach a unique element in this docker-compose.yml file, let’s get into that.

cat docker-compose.yml
services:
  wpname-mysql:
    container_name: wpname-mysql
    image: mysql:latest
    hostname: wpname-mysql
    restart: unless-stopped
    expose:
      - 3306
      - 33060
    extra_hosts:
      - "mysql:127.0.0.1"
    networks:
      backend:
        aliases:
          - mysql
    environment:
      - MYSQL_DATABASE=wordpress_databasename
      - MYSQL_USER=database_username
      - MYSQL_PASSWORD=database_password
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    command: ['mysqld']
    volumes:
      #- ./my.cnf:/etc/my.cnf
      - mysql:/var/lib/mysql
      - mysqldrun:/var/run/mysqld
    healthcheck:
      test: mysql -u"$${MYSQL_USER}" -p"$${MYSQL_PASSWORD}" -hmysql "$${MYSQL_DATABASE}" -e 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES LIMIT 1;'
      interval: 20s
      timeout: 5s
      retries: 10
      #start_period: 10s
  wpname-wordpress:
    container_name: wpname-wordpress
    depends_on:
      wpname-mysql:
        condition: service_healthy
    image: wordpress:fpm
    hostname: wpname-wordpress
    restart: on-failure
    expose:
      - 9000
    networks:
      backend:
        aliases:
          - wordpress
    extra_hosts:
      - "localhost:192.168.34.82"
    environment:
      - WORDPRESS_DB_HOST=mysqlhostname
      - WORDPRESS_DB_NAME=wordpress_databasename
      - WORDPRESS_DB_USER=database_username
      - WORDPRESS_DB_PASSWORD=database_password
      - WORDPRESS_TABLE_PREFIX=wp_
    links:
      - wpname-mysql
    volumes:
      - wordpress:/var/www/html:rw
      - mysqldrun:/var/run/mysqld
      #- ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
    healthcheck:
      test: [ "CMD", "php", "-r", "phpinfo();" ]
      interval: "20s"
      timeout: "5s"
      retries: 10
  wpname-nginx:
    container_name: wpname-nginx
    depends_on:
      wpname-wordpress:
        condition: service_healthy
      wpname-mysql:
        condition: service_healthy
    image: nginx:latest
    hostname: wpname
    domainname: domain.tld
    restart: unless-stopped
    links:
      - wpname-wordpress
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - wordpress:/var/www/html
    ports:
      - "80:80"
    expose:
      - 80
    networks:
      core01:
        mac_address: 02:42:ac:01:23:45
        ipv4_address: 192.168.34.82
        ipv6_address: 1000:2000:3000:4000:42:acff:fe1:2345
        aliases:
         - wpname.domain.tld
      backend:
        aliases:
         - wpname.domain.tld
    restart: on-failure
    expose:
      - 80
    links:
      - wpname-wordpress
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/snippets/realip.conf:/etc/nginx/snippets/realip.conf:ro
      - ./nginx/snippets/redirect.conf:/etc/nginx/snippets/redirect.conf:ro
      - wordpress:/var/www/html:rw
      - /etc/localtime:/etc/localtime:ro
    environment:
      UID: 1000
      GID: 1000
    entrypoint:
      - /bin/sh
      - -c
      - |
        usermod -u $${UID} nginx
        groupmod -g $${GID} nginx
        /docker-entrypoint.sh nginx -g 'daemon off;'
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 20s
      timeout: 5s
      retries: 10
  wpname-wpcli:
    container_name: wpname-wpcli
    depends_on:
      - wpname-mysql
      - wpname-wordpress
    image: wordpress:cli
    links:
      - wpname-mysql
      - wpname-wordpress
    networks:
      backend:
    environment:
      - WORDPRESS_DB_HOST=mysqlhostname
      - WORDPRESS_DB_NAME=wordpress_databasename
      - WORDPRESS_DB_USER=database_username
      - WORDPRESS_DB_PASSWORD=database_password
      - WORDPRESS_TABLE_PREFIX=wp_
    volumes:
      - wordpress:/var/www/html:rw
      - mysqldrun:/var/run/mysqld

networks:
  core01:
    external: true
  backend:

volumes:
  wordpress:
  mysqldrun:
  mysql:

Now you will need to create an “nginx” folder for the one map/bind that happens for a local target. All the rest of the storage areas use native Docker spaces that are a part of the modern Docker environment. Generally these are in “/var/lib/docker/volumes/…” and you can see more if you inspect the volume. In the “nginx” folder make this file or one customized to your needs.

cat nginx/default.conf
cat nginx/default.conf

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

	server_name wpname.domain.tld wpname.tld;
	index index.php index.html index.htm;
	root /var/www/html;

	gzip on;

	location / {
		try_files $uri $uri/ /index.php$is_args$args;
	}
	location ~ \.php$ {
		try_files $uri =404;
		fastcgi_split_path_info ^(.+\.php)(/.+)$;
		fastcgi_pass wpname-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;
	}
	location ~ /\.ht {
		deny all;
	}
	location = /favicon.ico { 
		log_not_found off;
		access_log off; 
	}
	location = /robots.txt { 
		log_not_found off;
		access_log off;
		allow all; 
	}
	location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
		expires max;
		log_not_found off;
	}
}

If you added a “frontend:” network to the bottom of the “docker-compose.yml” file and ran all of this on the same Desktop/Laptop host you were using, then, everything should resolve for you naturally.

You wouldn’t need to manually create a “macvlan” network, and you could remove the whole “core01” network part from “docker-compose.yml” (nginx service and networks at the bottom), adding a new listing just like “backend:” in the “nginx” service, named “frontend”, and at the bottom under “networks:”, again, listed just like “backend:”.

The WordPress CLI

We can use the following script to update our WordPress instance including core, plugins, and themes. User 33:33 allows us to run as the user for the instance, your’s might have a different one depending on how you might deviate from the images used in this guide.

#!/bin/bash
set +x
docker compose run --rm --user 33:33 wpname-wpcli core update
docker compose run --rm --user 33:33 wpname-wpcli plugin update --all
docker compose run --rm --user 33:33 wpname-wpcli theme update --all

That’s it, now, “chmod +x deploy_container.sh” and then “./deploy_container.sh” and watch your environment deploy! Enjoy!

Things I haven’t done yet:

  • Tried to use the WPCLI
  • Get Lets Encrypt working for the Stack
  • Deploy desired plug-ins “slipstream” style, so the environment doesn’t have to post mod it in

Leave a comment

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.