Docker · MySQL · Server · WordPress

Customise WordPress on Docker


In this guide, let’s go through the process of installing and configuring WordPress on a Docker container using docker-compose. The primary goals are to install a bare-bones install with the ability to customise wp-config.php, php.ini and my.cnf. There is an expectation that you are familiar with Docker, WordPress, PHP and MySQL configuration files.

Install WordPress on Docker

On the system hosting Docker, create a working directory for the WordPress installation.

# Create a working directory
mkdir example.com

# Change the current working directory
cd example.com

Inside the working directory create a docker-compose.yml configuration file for editing. I use Visual Studio Code with the Docker extension, as shown below, but any editor is fine.

# An example command to launch Visual Studio Code
code docker-compose.yml

Go to the official page for the WordPress Docker image on Docker Hub.

https://hub.docker.com/_/wordpress

Scroll down on the Description tab until you reach via docker stack deploy or docker-compose.

version: '3.1'

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql

volumes:
  wordpress:
  db:
Visual Studio Code editing docker-compose.yml
Visual Studio Code editing docker-compose.yml

Copy the code block for the Example stack.yml into your editor with docker-compose.yml open and save the file.

Back in the terminal, create and start the newly saved Docker composition.

# Build, create, start and attach the container services
docker-compose up

Once ready, the containers return this feedback.

wordpress_1 | Apache/2.4.38 (Debian) PHP/7.3.11 configured -- resuming normal operations
wordpress_1 | Command line: 'apache2 -D FOREGROUND'

A new WordPress installation should be ready at http://localhost:8080. Fill out all the relevant form of information and you’ll be at the WordPress administration page. Congratulations, WordPress is installed!

WordPress Install web form
WordPress Install

If you have any conflicts or issues with port 8080. Edit docker-compose.yml and change the 8080:80 ports configuration in the WordPress container to a different value such as 8500:80 and run docker-compose up again. You’ll also have to visit the new URL http://localhost:8500.

Resetting your WordPress Docker container

If at any time you wish to revert your container to a fresh install you’ll need to remove the storage volumes with these commands. Note, this cannot be undone.

# Stop and remove the containers
docker-compose down

# Delete both the wordpress and db file volumes
docker volume rm examplecom_db examplecom_wordpress

# Rebuild, recreate, start and attach the container services
docker-compose up

Improving docker-compose.yml

Renaming the database name, its user and password

The sample docker-compose.yml configuration is minimal and is not suitable for production. So the first thing you may want to do is replace the environment database name, user and password values for both the WordPress and DB containers. As our database user does not have significant permissions, this process requires the deletion of the existing database volume and any of its data.

WORDPRESS_DB_... values must match their MYSQL_... environment values.

  wordpress:
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: myblogdb
      WORDPRESS_DB_USER: wpinternal
      WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage
  db:
    environment:
      MYSQL_DATABASE: myblogdb
      MYSQL_USER: wpinternal
      MYSQL_PASSWORD: stencil-running-cargo-savage

Now we need to rebuild the MySQL database with these new values.

# Stop and remove the containers
docker-compose down

# Delete the db file volume
docker volume rm examplecom_db

# Rebuild, recreate, start and attach the container services
docker-compose up

To test the account and password changes run the following.

# Log into the database with the MySQL command-line client
docker-compose exec db mysql --user=wpinternal --password=stencil-running-cargo-savage myblogdb

Then in the mysql> prompt.

show tables;
quit;

Note that show tables; only return data if the WordPress installation is complete.

mysql> show tables;
+-----------------------+
| Tables_in_myblogdb    |
+-----------------------+
| wp_commentmeta        |
| wp_comments           |
| wp_links              |
| wp_options            |
| wp_postmeta           |
| wp_posts              |
| wp_term_relationships |
| wp_term_taxonomy      |
| wp_termmeta           |
| wp_terms              |
| wp_usermeta           |
| wp_users              |
+-----------------------+
12 rows in set (0.00 sec)

mysql> quit;
Bye

Add metadata labels

Labels are an optional means to apply metadata to Docker containers, images, volumes and networks. Generally, I use a minimal set of description labels for my containers but ignore the other options.

Labels use a distinctive syntax where the reverse domain name followed by the metadata name makes up the key. You can read more about this at Docker object labels but here are a couple of examples.

"com.example.description=A description for example.com"
"com.devtidbits.description=A description for this blog, The Developer's Tidbits"

In the docker-compose.yml I’ll add some labels to the containers and volumes.

version: "3.1"

services:
  wordpress:
    image: wordpress
    labels:
      - "com.example.description=Example WordPress site"
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpinternal
      WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage
      WORDPRESS_DB_NAME: myblogdb
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:5.7
    labels:
      - "com.example.description=Example WordPress database"
    restart: always
    environment:
      MYSQL_DATABASE: myblogdb
      MYSQL_USER: wpinternal
      MYSQL_PASSWORD: stencil-running-cargo-savage
      MYSQL_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - db:/var/lib/mysql

volumes:
  wordpress:
    labels:
      - "com.example.description=WordPress install"
  db:
    labels:
      - "com.example.description=MySQL datastore"

To apply labels to the volumes, they need to be deleted and rebuilt, resulting in the loss of their data.

# Stop and remove the containers
docker-compose down

# Delete both the wordpress and db file volumes
docker volume rm examplecom_db examplecom_wordpress

# Rebuild, recreate, start and attach the container services
docker-compose up

# Inspect the new volumes
docker volume inspect examplecom_db examplecom_wordpress
[
    {
        "CreatedAt": "2019-10-30T02:48:25Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "examplecom",
            "com.docker.compose.version": "1.24.1",
            "com.docker.compose.volume": "db",
            "com.example.description": "MySQL datastore"
        },
        "Mountpoint": "/var/lib/docker/volumes/examplecom_db/_data",
        "Name": "examplecom_db",
        "Options": null,
        "Scope": "local"
    },
    {
        "CreatedAt": "2019-10-30T02:48:25Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "examplecom",
            "com.docker.compose.version": "1.24.1",
            "com.docker.compose.volume": "wordpress",
            "com.example.description": "WordPress install"
        },
        "Mountpoint": "/var/lib/docker/volumes/examplecom_wordpress/_data",
        "Name": "examplecom_wordpress",
        "Options": null,
        "Scope": "local"
    }
]

Long-form syntax

The docker-compose.yml sample given on the WordPress docker hub page is intentionally compact. But for readability, I like to expand these configurations using the long-form syntax that was introduced in compose format 3.2. Firstly we need to update the version of docker-compose.yml, which is 3.1. You may as well update it to the newest version at the time of writing, 3.7.

version: "3.7"

The two configurations in our compose file we can express in long-form are ports and volumes.

Ports

    ports:
      - 8080:80

In the long-form syntax.

    ports:
      - target: 80
        published: 8080
        protocol: tcp

Volumes

The WordPress volume.

    volumes:
      - wordpress:/var/www/html
    volumes:
      - type: volume
        source: wordpress
        target: /var/www/html

The database volume.

    volumes:
      - db:/var/lib/mysql
    volumes:
      - type: volume
        source: db
        target: /var/lib/mysql

WordPress configurations – wp-config.php

One of the most important files for any WordPress installation is wp-config.php. In this Docker container though we can embed any configurations directly into docker-compose.yml. A complete list of these configurations is at the WordPress support article https://wordpress.org/support/article/editing-wp-config-php/.

It’s important to note that most of the required database connection configurations are irrelevant because these are in the container’s environment values. Any additional configurations can be added using the WORDPRESS_CONFIG_EXTRA variable. For example, these extra configs may be required and set to true in a production environment.

    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpinternal
      WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage
      WORDPRESS_DB_NAME: myblogdb
      WORDPRESS_CONFIG_EXTRA: |
        # Require SSL for admin and logins
        define( 'FORCE_SSL_ADMIN', false );
        # Disable plugin and theme update and installation
        define( 'DISALLOW_FILE_MODS', false );
        # Disable the plugin and theme editor
        define( 'DISALLOW_FILE_EDIT', false );

Any changes to the WORDPRESS_CONFIG_EXTRA variable requires the deletion of the existing wp-config.php file that is on the WordPress volume. Otherwise, the changes are not applied.

wordpress_1 | WARNING: environment variable "WORDPRESS_CONFIG_EXTRA" is set, but "wp-config.php" already exists
wordpress_1 | The contents of this variable will _not_ be inserted into the existing "wp-config.php" file.

To delete an existing wp-config.php in the container.

# Run the container in detached mode
docker-compose up --detach

# Run an optional test shell command on the wordpress container
docker-compose exec wordpress ls

# Delete the wp-config.php
docker-compose exec wordpress rm wp-config.php

# Restart the container to generate a new wp-config.php
docker-compose down && docker-compose up --detach

# View the content of the new wp-config.php
docker-compose exec wordpress cat wp-config.php

The output from cat wp-config.php should reveal the following appended additions.

// WORDPRESS_CONFIG_EXTRA
# Require SSL for admin and logins
define( 'FORCE_SSL_ADMIN', false );
# Disable plugin and theme update and installation
define( 'DISALLOW_FILE_MODS', false );
# Disable the plugin and theme editor
define( 'DISALLOW_FILE_EDIT', false );

Add configurations to PHP

WordPress is a PHP: Hypertext Preprocessor application, an old open-sourced language for the web that was very popular in the late 1990s and 2000s. PHP has its methods of configuration directives in the form of php.ini files.

Fix the 2 MB WordPress upload limit

Out of the box PHP and so also WordPress has a hard 2 MB upload limit for file forms. It means any images, videos or imported data uploads have a size limit when using WordPress. We can quickly fix that by creating our PHP configuration and binding it to the WordPress container.

WordPress 2 MB maximum upload file size example
WordPress 2 MB maximum upload file size

Create a new PHP configuration file, and name it docker-uploads.ini. Add the following configuration then save the changes.

# Allow HTTP file uploads
file_uploads = On

# Maximum size of an uploaded file
upload_max_filesize = 64M

# Maximum size of form post data
post_max_size = 64M

Update the docker-compose.yml to bind the docker-uploads.ini to the wordpress container and then restart the WordPress container.

    volumes:
      - type: volume
        source: wordpress
        target: /var/www/html
      - type: bind
        source: ./docker-uploads.ini
        target: /usr/local/etc/php/conf.d/docker-uploads.ini
# Rebuild our container
docker-compose down && docker-compose up --build

Visit the Upload New Media page to confirm the changes are active, http://localhost:8080/wp-admin/media-new.php

WordPress maximum upload file size updated to 64 MB
WordPress 64 MB maximum upload file size

Add configurations to Apache

The WordPress container usages a Debian installation of Apache HTTP Server to serve the web application. This combination uses a unique directory structure for Apache’s configuration files, but thankfully we can still bind our configurations to this setup.

Fix the Apache “could not reliably determine the server’s fully qualified domain name” notice

One problem we currently have in the log files that is fixable is this warning from Apache.

wordpress_1 | apache2: Could not reliably determine the server's fully qualified domain name, using 172.21.0.3. Set the 'ServerName' directive globally to suppress this message

Create a new Apache configuration file, and name it docker-servername.conf. Add the following configuration then save the changes.

# Hostname and port that the server uses to identify itself
ServerName localhost

Update the docker-compose.yml to bind the docker-servername.conf to the WordPress container and then rebuild.

    volumes:
      - type: volume
        source: wordpress
        target: /var/www/html
      - type: bind
        source: ./uploads.ini
        target: /usr/local/etc/php/conf.d/uploads.ini
      - type: bind
        source: ./docker-servername.conf
        target: /etc/apache2/conf-enabled/docker-servername.conf
# Rebuild our container
docker-compose down && docker-compose up --build

The Apache warning for the fully qualified domain name is gone.

Add configurations to MySQL

Fix the MySQL “TIMESTAMP with implicit DEFAULT value is deprecated” warning

Out of the box, MySQL 5.7 throws this warning on startup. We can quickly fix this by binding our own MySQL configuration file to the DB container.

db_1 | [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).

Create a new MySQL configuration file, and name it docker-fixes.cnf. Add the following configuration then save the changes.

[mysqld]
explicit_defaults_for_timestamp=on

Update the docker-compose.yml to bind the docker-fixes.cnf to the DB container and then restart.

    volumes:
      - type: volume
        source: db
        target :/var/lib/mysql
      - type: bind
        source: ./docker-fixes.cnf
        target: /etc/mysql/conf.d/docker-fixes.cnf
docker-compose down && docker-compose up

The TIMESTAMP with implicit DEFAULT value is deprecated warning should now not appear.

Add Adminer, a web interface for MySQL

For an optional, lightweight web interface for the MySQL database, you can attach an Adminer container to the composition. It also uses port 8080, which conflicts with our WordPress container, so I’ve swapped it to port 8070.

Update the docker-compose.yml to add the Adminer container to the composition and then restart.

services:
  adminer:
    image: adminer
    restart: always
    ports:
      - target: 8080
        published: 8070
        protocol: tcp
docker-compose down && docker-compose up

Visit http://localhost:8070 and use the username, password and database values saved to the MYSQL_DB… environment values to sign-in.

Adminer login form
Adminer login

Congratulations, now you know how to customise all the components your WordPress Docker installation. The final result for the docker-compose.yml is shown below and is also available at https://github.com/bengarrett/devtidbits.com.

version: "3.7"

services:
  wordpress:
    image: wordpress
    labels:
      - "com.example.description=Example WordPress site"
    restart: always
    ports:
      - target: 80
        published: 8080
        protocol: tcp
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpinternal
      WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage
      WORDPRESS_DB_NAME: myblogdb
      WORDPRESS_CONFIG_EXTRA: |
        # Require SSL
        define( 'FORCE_SSL_ADMIN', false);
        # Disable plugin and theme update and installation
        define( 'DISALLOW_FILE_MODS', false );
        # Disable the plugin and theme editor
        define( 'DISALLOW_FILE_EDIT', false );
    volumes:
      - type: volume
        source: wordpress
        target: /var/www/html
      - type: bind
        source: ./docker-uploads.ini
        target: /usr/local/etc/php/conf.d/docker-uploads.ini
      - type: bind
        source: ./docker-servername.conf
        target: /etc/apache2/conf-enabled/docker-servername.conf
  db:
    image: mysql:5.7
    labels:
      - "com.example.description=Example WordPress database"
    restart: always
    environment:
      MYSQL_DATABASE: myblogdb
      MYSQL_USER: wpinternal
      MYSQL_PASSWORD: stencil-running-cargo-savage
      MYSQL_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - type: volume
        source: db
        target: /var/lib/mysql
      - type: bind
        source: ./docker-fixes.cnf
        target: /etc/mysql/conf.d/docker-fixes.cnf
  adminer:
    image: adminer
    restart: always
    ports:
      - target: 8080
        published: 8070
        protocol: tcp

volumes:
  wordpress:
    labels:
      - "com.example.description=WordPress install"
  db:
    labels:
      - "com.example.description=MySQL datastore"

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s