WordPress Docker Stack

Run WordPress as a Docker Stack

Here I will show you how to run a full blown WordPress instance in a Docker (compose) Stack. All you need is to create the docker-compose.yml (compose.yml) file and run the wps-deploy.sh script. My setup contains all the important components you will need to run a WordPress Site. These are the main components of the docker stack:

Disclaimer: This content is based on my own research, various internet sources and a lot of try and error. I’m using this WordPress docker stack for a couple of productive websites without any major issues so far.
Please always backup your data! In my case I do the backup of all the WordPress data with a WordPress plugin.

Use this resource at your own risk and for your own fun!!

Preparation via wps-deploy.sh

With the help of the wps-deploy.sh script all the needed directory structure, config files and log files are getting created.

Main features of the script:

  • directory structure
    • config directory
    • data directory
    • log directory
  • create config files for the docker containers
    • mariadb_my.conf
    • redis.conf
    • php-conf.ini
    • apache2-security.conf
  • create .env file for docker-compose.yml
#!/bin/bash

echo ""
echo "*******************************************************"
echo "Welcome to the WPS (wordpress stack) deploy script!"
echo "*******************************************************"
echo ""

##############################################################
# prepare stack dir
##############################################################

# write wordpress stack path into variable
# please without the trailing slash
# example /opt/docker/stacks/wp01-draft
read -p "Enter stack path (without trailing slash): " WPS_PATH
echo "[x] WPS root dir: '$WPS_PATH'"
echo ""
echo ""

##############################################################
# create needed dirs
##############################################################

echo "*******************************************************"
echo "> Creating directories!"
echo "*******************************************************"

echo ""
# for config files
mkdir -p $WPS_PATH/config
echo "[x] dir created: '$WPS_PATH/config'"

# for data
mkdir -p $WPS_PATH/data
echo "[x] dir created: '$WPS_PATH/data'"

# for redis data
mkdir -p $WPS_PATH/data/redis
echo "[x] dir created: '$WPS_PATH/data/redis'"

# for mariadb data
mkdir -p $WPS_PATH/data/mariadb
echo "[x] dir created: '$WPS_PATH/data/mariadb'"

# for wordpress data
mkdir -p $WPS_PATH/data/wordpress
echo "[x] dir created: '$WPS_PATH/data/wordpress'"

# for log files
mkdir -p $WPS_PATH/log
echo "[x] dir created: '$WPS_PATH/log'"
echo ""
echo ""


##############################################################
# create config files
##############################################################

echo "*******************************************************"
echo "> Creating configuration files!"
echo "*******************************************************"

# create apache2 config file
cd $WPS_PATH/config
echo ""
{
  echo "# ./config/apache2_security.conf"
  echo "# APACHE configuration: /etc/apache2/conf-enabled/security.conf"
  echo "ServerTokens Prod"
  echo "ServerSignature Off"
  echo "TraceEnable Off"
} | sudo tee -a apache2_security.conf > /dev/null
echo "[x] file created: '$WPS_PATH/config/apache2_security.conf'"

# create mariadb config file
cd $WPS_PATH/config
{
  echo "# MARIADB configuration"
  echo "# ./config/mariadb_my.cnf"
  echo "[server]"
  echo "log_error=/var/log/mysql/server.log" 
} | sudo tee -a mariadb_my.cnf > /dev/null
echo "[x] file created: '$WPS_PATH/config/mariadb_my.cnf'"

# add redis config file
{
  echo "# REDIS configuration"
  echo "# ./config/redis_redis.conf"
  echo "logfile /var/log/redis/server.log"
} | sudo tee -a redis_redis.conf > /dev/null
echo "[x] file created: '$WPS_PATH/config/redis_redis.conf'"

# create php (wordpress) config file
{
  echo "; PHP configuration"
  echo "; ./config/wordpress_php-conf.ini"
  echo "file_uploads = On"
  echo "memory_limit = 300M"
  echo "upload_max_filesize = 100M"
  echo "post_max_size = 100M"
  echo "max_execution_time = 600"
  echo ""
  echo "; Disable PHP Error Reporting"
  echo "display_errors = Off"
  echo "log_errors = On"
  echo ""
  echo "; Error log file location"
  echo "error_log = /var/log/php/error.log"
} | sudo tee -a wordpress_php-conf.ini > /dev/null
echo "[x] file created: '$WPS_PATH/config/wordpress_php-conf.ini'"
echo ""
echo ""

##############################################################
# prepare log files
##############################################################

echo "*******************************************************"
echo "> Preparing log files!"
echo "*******************************************************"

cd $WPS_PATH/log
touch mariadb_server.log
echo ""
echo "[x] touched file: '$WPS_PATH/log/mariadb_server.log'"

touch redis_server.log
echo "[x] touched file: '$WPS_PATH/log/redis_server.log'"

touch wordpress_apache2-access.log
echo "[x] touched file: '$WPS_PATH/log/wordpress_apache2-access.log'"

touch wordpress_apache2-error.log
echo "[x] touched file: '$WPS_PATH/log/wordpress_apache2-error.log'"

touch wordpress_php-error.log
echo "[x] touched file: '$WPS_PATH/log/wordpress_php-error.log'"
echo ""
echo ""

chmod 666 *

##############################################################
# create .env file
##############################################################

echo "*******************************************************"
echo "> Creating .env file for docker-compose.yml!"
echo "*******************************************************"

cd $WPS_PATH

# define COMPOSE_PROJECT_NAME
# example: wp01-example.com
echo ""
read -p "Define variable COMPOSE_PROJECT_NAME (wpxx_example-com): " DEPLOY_COMPOSE_PROJECT_NAME
sudo sh -c "echo 'COMPOSE_PROJECT_NAME=$DEPLOY_COMPOSE_PROJECT_NAME' >> .env"
#echo "COMPOSE_PROJECT_NAME=$DEPLOY_COMPOSE_PROJECT_NAME" | sudo tee -a .env

# define WP_INSTANCE
# example: wp01
read -p "Define variable WP_INSTANCE (wp0x): " DEPLOY_WP_INSTANCE
sudo sh -c "echo 'WP_INSTANCE=$DEPLOY_WP_INSTANCE' >> .env"
#echo "WP_INSTANCE=$DEPLOY_WP_INSTANCE" | sudo tee -a .env

# define WORDPRESS_DB_NAME
# example: WORDPRESS_DB_NAME=8b420bb0_wp-db
# write uuid into variable
UUID_1=$(uuidgen)
# take the first part of the uuid into a different variable
UUID_1_1=${UUID_1:0:8}
sudo su -c "echo 'WORDPRESS_DB_NAME=${UUID_1_1}_wp-db' >> .env"
#echo "WORDPRESS_DB_NAME=${UUID_1_1}_wp-db" | sudo tee -a .env

# define WORDPRESS_TABLE_PREFIX
# example: WORDPRESS_TABLE_PREFIX=wp_
read -p "Define variable WORDPRESS_TABLE_PREFIX (wp_): " DEPLOY_WORDPRESS_TABLE_PREFIX
sudo su -c "echo 'WORDPRESS_TABLE_PREFIX=$DEPLOY_WORDPRESS_TABLE_PREFIX' >> .env"

# define WORDPRESS_DB_USER
# example: WORDPRESS_DB_USER=bacd36315127_wp-db-user
UUID_2=$(uuidgen)
# take the first part of the uuid into a different variable
UUID_2_1=${UUID_2:0:8}
sudo su -c "echo 'WORDPRESS_DB_USER=${UUID_2_1}_wp-db-user' >> .env"
#echo "WORDPRESS_DB_NAME=${UUID_2_1}_wp-db" | sudo tee -a .env

# define WORDPRESS_DB_PASSWORD
# example: WORDPRESS_DB_PASSWORD=018e46a5-c070-74a5-aa01-9fcd67629a87
UUID_3=$(uuidgen)
sudo su -c "echo 'WORDPRESS_DB_PASSWORD=$UUID_3' >> .env"
#echo "WORDPRESS_DB_PASSWORD=$UUID_3" | sudo tee -a .env
echo ""

# define MYSQL_ROOT_PASSWORD
# example: MYSQL_ROOT_PASSWORD=018e46a6-0c86-7717-9755-96b4b0a5ee71
UUID_4=$(uuidgen)
sudo su -c "echo 'MYSQL_ROOT_PASSWORD=$UUID_4' >> .env"
#echo "MYSQL_ROOT_PASSWORD=$UUID_4" | sudo tee -a .env
echo ""

# define REDIS_PASSWORD
# example: REDIS_PASSWORD=ebe2c513-5e58-4d4b-8ec2-cf017a92475a
UUID_5=$(uuidgen)
sudo su -c "echo 'REDIS_PASSWORD=$UUID_5' >> .env"
#echo "REDIS_PASSWORD=$UUID_5" | sudo tee -a .env
echo ""

# define WP_URL
read -p "Define variable WP_URL (example.com): " DEPLOY_WP_URL
sudo su -c "echo 'WP_URL=https://$DEPLOY_WP_URL' >> .env"
#echo "WP_URL=https://$DEPLOY_WP_URL" | sudo tee -a .env

# define WP_TITLE
read -p "Define variable WP_TITLE (Organisation Name): " DEPLOY_WP_TITLE
sudo su -c "echo 'WP_TITLE=$DEPLOY_WP_TITLE' >> .env"
#echo "WP_TITLE=$DEPLOY_WP_TITLE" | sudo tee -a .env

# define WP_ADMIN
# example: WP_ADMIN=wp-admin-khp
read -p "Define variable WP_ADMIN (wp-admin-orgname): " DEPLOY_WP_ADMIN
sudo su -c "echo 'WP_ADMIN=$DEPLOY_WP_ADMIN' >> .env"
#echo "WP_ADMIN=$DEPLOY_WP_ADMIN" | sudo tee -a .env

# define WP_ADMIN_PASS
# example: WP_ADMIN_PASS=018e58b2-d2bd-7d70-8cd0-07a346d2042a
UUID_6=$(uuidgen)
DEPLOY_WP_ADMIN_PASS=$UUID_6
sudo su -c "echo 'WP_ADMIN_PASS=$DEPLOY_WP_ADMIN_PASS' >> .env"
#echo "WP_ADMIN_PASS=$UUID_6" | sudo tee -a .env

# define WP_ADMIN_EMAIL
# example: WP_ADMIN_EMAIL=admin@domain.com
read -p "Define variable WP_ADMIN_EMAIL: " DEPLOY_WP_ADMIN_EMAIL
sudo su -c "echo 'WP_ADMIN_EMAIL=$DEPLOY_WP_ADMIN_EMAIL' >> .env"
#echo "WP_ADMIN_EMAIL=$DEPLOY_WP_ADMIN_EMAIL" | sudo tee -a .env
echo ""

##############################################################
# create syntax for basic wordpress install via wp-cli 
##############################################################

echo ""
echo "*******************************************************"
echo "> Basic wordpress install via WP-CLI!"
echo "*******************************************************"

echo ""
echo "Name of the hosting instance: $DEPLOY_WP_INSTANCE"
echo "Name of the docker compose project: $DEPLOY_COMPOSE_PROJECT_NAME"
echo ""
echo "Please connect to the wp-cli container via the following syntax:"
echo "sudo docker exec -it ${DEPLOY_COMPOSE_PROJECT_NAME}_wp-cli /bin/bash"
echo ""
echo "Wordpress Language: 'de_DE':"
echo "wp core install --path=\"/var/www/html\" --url=\"https://$DEPLOY_WP_URL\" --title=\"$DEPLOY_WP_TITLE\" --admin_user=\"$DEPLOY_WP_ADMIN\" --admin_password=\"$DEPLOY_WP_ADMIN_PASS\" --admin_email=\"$DEPLOY_WP_ADMIN_EMAIL\" --locale=\"de_DE\" --skip-email"
echo ""
echo "Wordpress Language: 'en_US':"
echo "wp core install --path=\"/var/www/html\" --url=\"https://$DEPLOY_WP_URL\" --title=\"$DEPLOY_WP_TITLE\" --admin_user=\"$DEPLOY_WP_ADMIN\" --admin_password=\"$DEPLOY_WP_ADMIN_PASS\" --admin_email=\"$DEPLOY_WP_ADMIN_EMAIL\" --skip-email"
echo ""
echo "Thanks for running this deployment script. HAVE FUN!"
echo ""
echo ""

To run the script you have to add the execution right (x) and give sudo permissions. Here is how to do this:

sudo +x wps-deploy.sh
sudo ./wps-deploy.sh

Here you can see how the script looks like while running. For this run we just use dummy data.

output of the script....

Data structure and config files

To give you a look and feel about the used config files please see the following examples.

Data structure inside the root folder of the docker compose stack.

.
├── config
│   ├── apache2_security.conf
│   ├── mariadb_my.cnf
│   ├── redis_redis.conf
│   └── wordpress_php-conf.ini
├── data
│   ├── mariadb
│   ├── redis
│   └── wordpress
├── docker-compose.yml
├── .env
├── log
│   ├── mariadb_server.log
│   ├── redis_server.log
│   ├── wordpress_apache2-access.log
│   ├── wordpress_apache2-error.log
│   └── wordpress_php-error.log
└── wps-deploy.sh

apache2_security.conf

# /etc/apache2/conf-enabled/security.conf
ServerTokens Prod
ServerSignature Off
TraceEnable Off

mariadb_my.cnf

Manages additional MariaDB configuration for the redis container.

# MariaDB configuration
# ./config/mariadb_my.cnf
[server]
log_error=/var/log/mysql/server.log

redis_redis.conf

Manages additional redis configuration for the redis container.

# REDIS configuration
# ./config/redis_redis.conf
logfile /var/log/redis/server.log

wordpress_php-conf.ini

Manages additional PHP configuration for the WordPress container.

; PHP configuration
; ./config/wordpress_php-conf.ini
file_uploads = On
memory_limit = 300M
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 600

; Disable PHP Error Reporting
display_errors = Off
log_errors = On

; Error log file location
error_log = /var/log/php/error.log

.env

Provides data for docker-compose.yml stack configuration .

COMPOSE_PROJECT_NAME=wp01_yourdomain-org
WP_INSTANCE=wp01
WORDPRESS_DB_NAME=e07d2057_wp-db
WORDPRESS_TABLE_PREFIX=wp_
WORDPRESS_DB_USER=542ba348_wp-db-user
WORDPRESS_DB_PASSWORD=dac7133d-5b1a-4774-8fa5-3a653b175579
MYSQL_ROOT_PASSWORD=1d908589-3a8a-4d84-ab78-41c530d02a16
REDIS_PASSWORD=5ddafb51-326f-4b72-90d0-bdff2c41861d
WP_URL=https://yourdomain.org
WP_TITLE=yourdomain.org
WP_ADMIN=wp-admin-user
WP_ADMIN_PASS=60af0226-a07d-4095-8eb5-41c33a2c577b
WP_ADMIN_EMAIL=admin@yourdomain.org

Docker stack via docker-compose.yml

Cause Security is one of my main concerns I like to always use the latest tag for my docker images. So far I did not had and any issues with breaking stuff.

---
#version: "3.8"
name: ${COMPOSE_PROJECT_NAME}
services:
# *******************************************
# WORDPRESS (website)
# reverse-proxy port: 127.0.0.1:65180 (HTTP)
# *******************************************
  wordpress:
    image: wordpress:latest
    container_name: ${COMPOSE_PROJECT_NAME}_wp-app
    hostname: ${WP_INSTANCE}_wp-app
    restart: unless-stopped
    ports:
      - "127.0.0.1:65180:80"
    volumes:
      - ./data/wordpress:/var/www/html   # WordPress data dir
      - ./config/wordpress_php-conf.ini:/usr/local/etc/php/conf.d/conf.ini   # PHP config file
      - ./config/apache2_security.conf:/etc/apache2/conf-enabled/security.conf # Apache2 config file
      - ./log/wordpress_php-error.log:/var/log/php/error.log   # PHP error log
      - ./log/wordpress_apache2-error.log:/var/log/apache2/error.log    # Apache2 error log
      - ./log/wordpress_apache2-access.log:/var/log/apache2/access.log   # Apache2 access log
    environment:
      TZ: Europe/Vienna
      WORDPRESS_DB_HOST: mariadb
      WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
      WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
    depends_on:
      - mariadb
      - redis

# *******************************************
# WORDPRESS (CLI)
# *******************************************
  wpcli:
    image: wordpress:cli
    container_name: ${COMPOSE_PROJECT_NAME}_wp-cli
    hostname: ${WP_INSTANCE}_wp-cli
    volumes:
      #- ./config/wordpress_php-conf.ini:/usr/local/etc/php/conf.d/conf.ini
      - ./data/wordpress:/var/www/html   # WordPress data dir
    user: "33" # default wpcli user, username "xfs" with the ID "33"
    #entrypoint: wp # comand executed every start
    tty: true  # Enable terminal interaction
    stdin_open: true  # Keep STDIN open even if not attached
    command: /bin/bash
    environment:
      TZ: Europe/Vienna
      HOME: /tmp
      WORDPRESS_DB_HOST: mariadb
      WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
      WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
    depends_on:
      - mariadb
      - redis
      - wordpress

# *******************************************
# REDIS
# *******************************************
  redis:
    image: redis:latest
    container_name: ${COMPOSE_PROJECT_NAME}_redis
    hostname: ${WP_INSTANCE}_redis
    restart: unless-stopped
    expose:
      - 6379
    volumes:
      - ./dаta/redis:/data   # Redis data dir
      - ./config/redis_redis.conf:/usr/local/etc/redis/redis.conf   # Redis config
      - ./log/redis_server.log:/var/log/redis/server.log
    command: redis-server /usr/local/etc/redis/redis.conf   # Enable additional redis config
    environment:
      TZ: Europe/Vienna
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}

# *******************************************
# PHPMYADMI
# reverse-proxy port: 127.0.0.1:65181 (HTTP)
# *******************************************
  phpmyadmin:
    image: phpmyadmin:latest
    container_name: ${COMPOSE_PROJECT_NAME}_pma
    hostname: ${WP_INSTANCE}_pma
    restart: unless-stopped
    ports:
      - "127.0.0.1:65181:80"
    environment:
      TZ: Europe/Vienna
      PMA_HOST: mariadb
      PMA_PORT: 3306
      PMA_ARBITRARY: 1
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      UPLOAD_LIMIT: 100M
    depends_on:
      - mariadb

# *******************************************
# DATABASE
# *******************************************
  mariadb:
    image: mariadb:latest
    container_name: ${COMPOSE_PROJECT_NAME}_mariadb
    hostname: ${WP_INSTANCE}_mariadb
    restart: unless-stopped
    volumes:
      #- ./wp-data:/docker-entrypoint-initdb.d
      - ./data/mariadb:/var/lib/mysql   # Mariadb data dir
      - ./log/mariadb_server.log:/var/log/mysql/server.log   # Mariadb server log
      - ./config/mariadb_my.cnf:/etc/mysql/my.cnf   # Mariadb config file
    environment:
      TZ: Europe/Vienna
      MYSQL_DATABASE: ${WORDPRESS_DB_NAME}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_USER: ${WORDPRESS_DB_USER}
      MYSQL_PASSWORD: ${WORDPRESS_DB_PASSWORD}
    command:
    - '--character-set-server=utf8mb4'
    - '--collation-server=utf8mb4_unicode_520_ci'

Manage docker stack

After creating the docker-compose.yml (or compose.yml) file you can start the docker stack with the following command.

sudo docker compose up -d

To check the logs of the docker stack please use this command.

sudo docker compose logs -ft

To check the resource consumption of the docker containers within the docker stack use the following command.

sudo docker compose stats

WP-CLI Syntax

If you don’t want to do the basic installation process via the web-base WordPress wizard you can do it via the terminal and the WP-CLI syntax. First you have to connect to the WP-CLI docker container.

sudo docker exec -it <your-docker-compose-name>_wp-cli /bin/bash

Installing WordPress via WP-CLI.

wp core install --path="/var/www/html" --url="www.yourdomain.org" --title="NEW PAGE TITLE" --admin_user="wp-admin-user" --admin_password="55e3807b-0bf1-42e5-b579-690284038d6a" --admin_email="admin@yourdomain.org" --skip-email

Post deployment tasks

After deploying the docker stack and doing the basic WordPress installation we have to do some post deployment tasks.

WordPress (wp-config.php)

If you do decided to use the redis component of the stack you have to put some basic configuration inside the wp-config.php file.

/* REDIS CONFIG */
// Docker hostname or IP address of your Redis server
define('WP_REDIS_HOST', 'redis');
// Port number of your Redis server
define('WP_REDIS_PORT', '6379');
// Redis server password (from docker-compose.yml via .env file)
define('WP_REDIS_PASSWORD', getenv('REDIS_PASSWORD'));
// change the prefix and database for each site (optional)
define( 'WP_REDIS_PREFIX', 'wp01' );
// 0-15
define( 'WP_REDIS_DATABASE', 0 );
// reasonable connection and read+write timeouts (optional)
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );

Redefine memory limits for WordPress. This step is completly optional. The default WP_MEMORY_LIMIT value is 40MB.

/* redefine MEMORY LIMITS */
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');

If you found any mistakes or encounter interesting error please do not hesitate contacting me via contact form.

Have fun!