Dockerizing An Existing WordPress Site

After dockerizing this blog with 15 years worth of content a couple of days ago and pushing it into a virtual machine running in a data-center in Helsinki, here’s an account of how to do this in case you are playing with the thought of doing something similar. As with many other things, everything one needs to know can be found on the web, but there is not a single resource that puts the whole process together. So let’s change this.

A High Level Migration Process Overview

WordPress stores its data in two places: All text and configuration information is held in a MySQL database, while images and downloaded plugins are stored in a directory in the WordPress installation itself. To keep the adrenaline level and the migration downtime as low as possible, it’s best to export the data first and then experiment on the new system with a different domain name. Once everything is in place, a final data export / import is then performed and finally, the domain name is changed in the container setup and in the DNS server, so new requests go to the new site. Below, I will refer to the two servers as the source server and destination server.

Setting the DNS TTL value

Before doing anything on the servers, one should have a look at the Time To Live (TTL) value of the domain name in the DNS server. Ideally, it should be set to 60 seconds, so changes of the IP address are propagated through the DNS chain quickly. In many cases, this value is set several minutes or hours, so it’s important to change the parameter to a value as low as possible right at the beginning of the procedure. In my case, the DNS TTL of the domain name of my blog was set to 1 hour. Changing it to 1 minute meant that I had to wait with the final migration for at least one hour after changing the value. That was not a problem in practice, because it took quite a bit more time to get everything set-up anyway.

Data Export

The first step of the actual migration procedure is to get a copy of the configuration and user data from the source site. The data stored in the MySQL database can be exported to a text file with the following command:

mysqldump -uDB_USER DB_NAME > /home/USERNAME/db-dump.sql

The DB_USER and DB_NAME can be found in the WordPress configuration file (wp-config.php). Next, all content (e.g. images) and plugins that have been installed over the years have to exported to a tar file. Let’s assume /var/www/html is the base of the WordPress installation on the source server:

cd /var/www/html/wp-content
tar cvzf /home/USERNAME/wp-content.tar.gz .

And that’s it on the source server, all data is now safely backed-up without any service interruption. Both files can now be copied to the destination system.

Getting a WordPress Installation Up- And Running

The next step in the process is to set up a WordPress installation in containers on the destination system. Typically, this involves setting up a reverse http proxy as described in this and in this post (pick one solution of your liking). Once that is done, create a standard WordPress/MySQL container setup behind the reverse proxy. This is a bit of a tricky thing to get right because my ideas of how containers should be used and the ideas of the WordPress project are a bit different. Let me explain:

On Docker Hub, WordPress gives a docker-compose script that puts the complete WordPress code into a volume:

volumes:   
  - wordpress:/var/www/html

That means that any future docker image update with docker pull wordpress:latest will not overwrite the WordPress code. Only the web server and PHP parts will be updated. I was puzzled for a long time if this is really the way things should be done!? In an ideal world, the code in a container should be updated with a container update. But WordPress predates containers and insists on updating itself and potentially adapts the database structure in the process. So better to leave it this way and let WordPress update itself via the standard automated WordPress update procedure that has nothing to do with the container. The docker pull command is then only used for security updates of the non-Wordpress specific parts of the container. To make a long story short, here’s my docker-compose.yml file that I used for this blog. It is based on the settings required for the the reverse proxy as described in this post:

version: '3.1'
 services:
 wp:
     image: wordpress:latest
     restart: always
     environment:
       WORDPRESS_DB_HOST: db
       WORDPRESS_DB_USER: XXXXXXX
       WORDPRESS_DB_PASSWORD: XXXXXXX
       WORDPRESS_DB_NAME: XXXXXXX
       WORDPRESS_TABLE_PREFIX: wp_wlm_blog_
       VIRTUAL_HOST: exp0000.test-domain.de
       LETSENCRYPT_HOST: exp0000.test-domain.de
       LETSENCRYPT_EMAIL: xxxx@yyyy.zz
     volumes:
       - ./wordpress:/var/www/html

 db:
     image: mysql:5.7
     restart: always
     environment:
       MYSQL_DATABASE: XXXXXXX
       MYSQL_USER: XXXXXXX
       MYSQL_PASSWORD: XXXXXXX
       MYSQL_ROOT_PASSWORD: XXXXXXX
     volumes:
       - ./db:/var/lib/mysql

 networks:
     default:
       external:
         name: webproxy

I’ve marked a couple of things in blue that require specific attention. Once upon a time, I decided to use a WordPress non-default table prefix as I only had one database available on a hosted system. This is why I included the WORDPRESS_TABLE_PREFIX environment variable which is otherwise not necessary.

To get a setup that I can easily move to a different host, the script above instructs Docker to create the volume for the database and the volume for the WordPress php/html files in the project directory by adding a ‘./’ in front of the volume names. This way, a tar of the project directory will save everything. Extracting the file on a different server then migrates the blog without requiring any further actions.

As the source server is still serving requests, it is necessary to use a different domain name on the target server for the time being. And here comes an important part: WordPress stores the domain name in many places in the SQL database so it is necessary to replace all occurrences of the domain name in the db-dump.sql file with the temporary domain name. Especially for plugins and widgets, the domain name is stored in ‘serialized’ form which includes a length indicator. This means that the length of the temporary domain name MUST MATCH the length of the real domain name. If the domain name lengths are different, your widgets will not show up and the site will look funny. And here is the command to find and replace all occurrences of the domain name in the SQL file:

# blog.wirelessmoves.com
# replaced with:
# exp0000.test-domain.de

sed -i 's+blog.wirelessmoves.com+exp0000.test-domain.de+g' db-dump.sql

If you haven’t done so already, create a entry for exp0000.test-domain.de on the DNS server at this point.

At this point, it is important to configure the same database name in the docker-compose.yml file as is used on the source server! Once done, the WordPress and MySQL containers have to be started once, so the named volumes for the two containers are created and populated with default data:

docker-compose up -d

It’s not necessary to go through the WordPress installation dialog in the web browser, one should just check if the containers come up and the WordPress installation dialog is shown in the web browser.

Data Import

At this point the setup is ready to receive the backup data. Restoring the text based database backup in db-dump.sql to the database can be done by pushing its content to the mysql command that runs inside the database container. This requires the database container to be up and running:

cat db-dump.sql | docker exec -i XXXXXXXX-db_1 sh -c '/usr/bin/mysql -u root --password="$MYSQL_ROOT_PASSWORD" MYSQL_DATABASE'

The two parts in the command marked in blue depend on the settings made in the docker-compse.yml file above and needed to be adapted accordingly. If the command terminates without an error message, the data is in the database, and all previous content has been deleted.

In the next and final step to get the test data into the target setup, the contents of the tar file has to be restored into the WordPress docker volume. This is done as follows:

cd wordpress  # the directory name of the named volume in the project directory
rm -rf wp-content
mkdir wp-content
cd wp-content

tar xvzf /PATH-OF-PROJECT-DIR/wp-content.tar.gz

And that’s it. I’m not sure if it’s necessary to restart the containers, but it doesn’t hurt, either:

docker-compose down
docker-compose up -d

If everything has gone according to plan, the blog is now available under the temporary domain name over HTTPS. Check that all plugins, widgets, background images, links to posts, etc., work as desired. Nothing should need to be configured and everything behaves as on the source server. If not, one can take all the time that is required to change the configuration and start over by deleting the db and the wordpress volume directories.

And Finally…

Once everything works as desired, it’s time to do the real transfer with the original domain name. This may requires another backup and transfer of the database and a backup of the files in the wp-content directory before the IP address for the domain is changed in the DNS server. The steps to activate the target server then look as follows:

# Copy the latest backup to the target machine

# Run the db import command again
# Delete/recreate the wp-content directory, untar files

docker-compose down

# Change the domain name in the docker-compose.yml file (2 locations)

# Change the IP address of the domain name in the DNS server.

# Wait for the time to live (hopefully set set to 1 minute earlier) to expire

docker-compose up -d

# Letsencrypt will get the certificate for the real domain name.

# The blog is up and running on the target server

Emergency Rollback

If something has gone wrong that can’t be fixed quickly, one can always roll-back to the old server by changing the IP address in the DNS server again. As the time to live is set to 1 minute, requests to the blog will go back to the old server after 60 seconds. One can then analyze what has gone wrong and try again.

Final Words

Agreed, this is not the simplest procedure I have ever described on the blog. However, on can tweak things for as long as necessary while the blog still runs on the source server. And in case the migration fails, a fallback can be performed within 60 seconds so I think it is a sound procedure.