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