I’ve been running two WordPress blogs for some time and my biggest regret is that they are not running in Docker containers. If I did the right thing in the beginning, I wouldn’t have to worry about whether or not the server upgrade will be safe, or will I be able to recall server configuration when time to migrate comes. I actually would be able to spin up local blog replica, run some experiments on it (new settings, features or design change) and decide whether or not I want move that change into ‘production’.
However, it’s never too late. I’m reluctant to make a big change on the real server without prior tests, so today I’ll try to create local Docker replica of one of my blogs and see how that goes.
Step 0: The idea
The overall process should be relatively simple, as WordPress already has its own Docker image. What’s more, they have a working example of docker-compose.yml file that spins up WordPress container along with mysql, so we have a nice starting point:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
version: '3.1' services: wordpress: image: wordpress ports: - 8080:80 environment: WORDPRESS_DB_PASSWORD: example mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: example |
I think attaching my blog’s backup (content + mysql data) to containers would actually get most of the job done. There might be some issues with WordPress expecting true domain name while running on localhost, but we might be able to do something about it. So let’s begin.
Step 1: Backup the blog
What I like about WordPress is how easy it is to take its backup. Archive www
folder, zip database’s dump, and it’s done. Surprisingly, this is also the thing that I don’t like about WordPress, as I’d rather prefer having clear separation of application from its configuration and data. It looks like there’s some sort of separation, but I’m not sure I can trust it.
So, let’s tar
WordPress’s web folder and make mysqldump
for codeblog.dotsandbrackets.com:
1 2 |
$ tar -czf codeblog.dotsandbrackets.com.`date +%Y%m%d-%H%M%S`.tar.gz ~/codeblog.dotsandbrackets.com # codeblog.dotsandbrackets.com.20170808-0233746.tar.gz |
1 2 3 |
$ mysqldump --databases $DB_NAME --add-drop-table -u **** -p **** | gzip > codeblog.dotsandbrackets.com.`date +%Y%m%d-%h%M%S`.sql.gz # Enter password: # produces codeblog.dotsandbrackets.com.2070808-024302.sql.gz |
After these files ended up on local machine, we can move to the step two.
Step 2: Restore the website in a container
I checked WordPress’s Dockerfile and it seems that it will update wp-config.php file with environmental variables passed to container. Net effect is that we won’t have to restore mysql database with the same credentials as we used ‘in production’ and use new ones instead. We even can use root
user (default choice for WP Dockerfile).
Very same Dockerfile says that it stores Wordpress’s content in /var/www/html
(which is a volume, btw), so I can simply mount my backed up content to that path.
Finally, I’m not comfortable putting any credentials into files that most likely will end up in source control, so I’ll put them into environmental variables instead, e.g.:
1 2 3 |
echo "export WP_DB_NAME='****'" >> ~/.profile echo "export WP_DB_USER_PASSWORD='****'" >> ~/.profile source ~/.profile |
Now, untar web content, create initial version of docker-compose.yml file with empty database for now and try to start it:
1 2 3 |
$ tar -xzf codeblog/codeblog.dotsandbrackets.com.20170808-023746.tar.gz \ --strip-components=2 # skip /home/%USER% path component # Writes to `pwd`/codeblog.dotsandbrackets.com |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
version: '2' services: wordpress: image: wordpress environment: WORDPRESS_DB_PASSWORD: ${WP_DB_USER_PASSWORD} WORDPRESS_DB_NAME: ${WP_DB_NAME} volumes: - ./codeblog.dotsandbrackets.com:/var/www/html mysql: image: mysql environment: MYSQL_ROOT_PASSWORD: ${WP_DB_USER_PASSWORD} MYSQL_DATABASE: ${WP_DB_NAME} |
1 2 3 |
docker-compose up -d # Give ~20-30 seconds to finish open http://127.0.0.1 |
It did start, but we can’t tell if that install page is taken from ‘our’ content or from one that came with container. We need some data. Let’s shut down the web site and go to find some:
1 |
docker-compose down |
Step 3: Restore the database
Googling into mysql’s Dockerfile reveals wonderful picture: whatever we mount to /docker-entrypoint-initdb.d/
path inside of mysql container will be executed when container starts for the first time. It even understands archives! So theoretically, mounting mysql dump to that path should do the trick. Let’s try that. mysql
service definition changes just a little bit:
1 2 3 4 5 6 7 |
mysql: image: mysql environment: MYSQL_ROOT_PASSWORD: ${WP_DB_USER_PASSWORD} MYSQL_DATABASE: ${WP_DB_NAME} volumes: - ./codeblog.dotsandbrackets.com.20170808-024302.sql.gz:/docker-entrypoint-initdb.d/backup.sql.gz |
Repeat startup process:
1 2 3 |
docker-compose up -d # give it some time to finish open http://127.0.0.1 |
Cool! However, there’s one problem. If I try to login, it’ll redirect me to codeblog.dotsandbrackets.com. It’s clearly using hardcoded domain name and we need to fix that.
Step 4. Fix web site address
Quick googling into a problem immediately shows that the following sql queries might help:
1 2 3 4 |
UPDATE wp_options SET option_value = replace(option_value, 'http://www.oldurl', 'http://www.newurl') WHERE option_name = 'home' OR option_name = 'siteurl'; UPDATE wp_posts SET guid = replace(guid, 'http://www.oldurl','http://www.newurl'); UPDATE wp_posts SET post_content = replace(post_content, 'http://www.oldurl', 'http://www.newurl'); UPDATE wp_postmeta SET meta_value = replace(meta_value,'http://www.oldurl','http://www.newurl'); |
mysql’s docker-entrypoint-initdb.d
path can accept many files (in alphabetical order), so if I replace oldurl
with codeblog.dotsandbrackets.com
and www.newurl
with 127.0.0.1
, put the result into migrate.sql
and mount it with - ./migrate.sql:/docker-entrypoint-initdb.d/migrate.sql
, the problem should be solved. Let’s give it a try.
Migrate file:
1 2 3 4 |
UPDATE wp_options SET option_value = replace(option_value, 'https://codeblog.dotsandbrackets.com', 'http://127.0.0.1') WHERE option_name = 'home' OR option_name = 'siteurl'; UPDATE wp_posts SET guid = replace(guid, 'https://codeblog.dotsandbrackets.com','http://127.0.0.1'); UPDATE wp_posts SET post_content = replace(post_content, 'https://codeblog.dotsandbrackets.com', 'http://127.0.0.1'); UPDATE wp_postmeta SET meta_value = replace(meta_value,'https://codeblog.dotsandbrackets.com','http://127.0.0.1'); |
New volumes
component in mysql service:
1 2 3 |
volumes: - ./codeblog.dotsandbrackets.com.20170808-024302.sql.gz:/docker-entrypoint-initdb.d/backup.sql.gz - ./migrate.sql:/docker-entrypoint-initdb.d/migrate.sql |
Now, restart the web site:
1 2 3 4 |
docker-compose down docker-compose up # Give it some time open https://127.0.0.1 |
This time it can navigate to login dialog and even login! But Jetpack plugin looks sad. I think for now I’ll just shut it down.
However, there’s bigger problem. Clicking on any article causes 404:
I vaguely remember configuring something in blog’s nginx properties when I installed it. Probably it’s time to head back to blog’s host and check out what was that.
Step 5. Fixing the links
OK, the problem’s just gotten a little bit bigger. Configuration line that made the links to work was try_files
nginx directive:
1 |
try_files $uri $uri/ /index.php?q=$request_uri&$args; |
But WordPress image that I used comes with Apache, not nginx. Apache does have similar configuration option, but I’m not that familiar with Apache to begin with, so let’s find another way.
WordPress image also comes with fpm configuration, which allows me to attach custom web server to it. I also found rault/nginx-wordpress image that sits and waits to be connected to wordpress:fpm
image. It actually might help.
Again, before making any changes to YAML file, stop running containers first:
1 |
docker-compose down |
Then, add new service definition to docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
version: '2' services: nginx: image: raulr/nginx-wordpress ports: - 80:80 volumes: - ./codeblog.dotsandbrackets.com:/var/www/html wordpress: image: wordpress:fpm environment: WORDPRESS_DB_PASSWORD: ${WP_DB_USER_PASSWORD} WORDPRESS_DB_NAME: ${WP_DB_NAME} volumes: - ./codeblog.dotsandbrackets.com:/var/www/html mysql: image: mysql environment: MYSQL_ROOT_PASSWORD: ${WP_DB_USER_PASSWORD} MYSQL_DATABASE: ${WP_DB_NAME} volumes: - ./codeblog.dotsandbrackets.com.20170808-024302.sql.gz:/docker-entrypoint-initdb.d/backup.sql.gz - ./migrate.sql:/docker-entrypoint-initdb.d/migrate.sql |
Finally, start containers and keep all possible fingers crossed, hoping that it will work:
1 2 |
docker-compose up -d open http://127.0.0.1 |
JetPack plugin is complaining on being in a sandbox again, but after disabling it one more time everything seems to be working now.
I clicked through the most obvious links, it all seems to be OK. In Docker and OK!
Conclusion
Moving existing WordPress into Docker containers is doable. Most of the process actually was quite straightforward and it’s only web server configuration step (probably specific to my site only) that puzzled me for a moment. I’ll spend some time looking for missing bits of configuration (e.g. host names), testing and measuring performance. But once I’m happy with all of that, I’m definitely moving that into ‘production’.
Great article, really useful for me having Docker experience already but using this as a neat shortcut to getting a WordPress install ‘dockerised’ for local development. Many thanks!
You are welcome!
Hi Pav. I just screwed up by signing up as a volunteer webmaster for a WordPress site. I never used WordPress before but it seems pretty accessible. I sign in on the site however and i see red notifications everywhere. Nothing has been updated for an eternity, which, i’m guessing leaves a bunch of vulnerabilities wide open.
So i’d like to update wordpress and the different plugins that the site uses. But i’m also a little bit afraid that I’ll mess up something in the process.
I took a back up of the site by ssh’ing onto the site and creating a sqldump and copying everything from /www on to my local machine.
But I still don’t really trust myself or my backup. So I’d like to verify, that I have an intact backup and was thinking maybe I could spin everything up in a Docker container.
I tried spinning it up using your docker-compose settings. But each time i spin up the docker container with the data I still see the install page which I’m guessing means that i’m doing something wrong. Also I’m getting ‘MySQL Connection Error: (2002) Connection refused’. I tried searching for a solution but I couldn’t find anything to get further.
– Hoping you’ll help me get a little further 🙂
Hi Emma,
I can’t tell what’s wrong in it without seeing it myself, but as the first step I’d check if mysql hostname, login and password in
wp-config.php
file are correct. Maybe there was a typo or something in docker-compose settings, so either mysql or wordpress container didn’t receive the credentials.Hey Pav, this article was just brilliant to figure out a smooth workflow to containerize my client’s website. Really nice and concise article, easy to follow. Hats off and many thanks — you saved me a lot of time here!
Execllent article man! Thx!
This was very helpful to me today. It looks though like you’re not mapping a MySQL volume for /var/lib/mysql in the container, so if you ever did
docker-compose down
, you’d lose all your changes since you spun up the containers, right?Oh shi… Yup, you’re right. Missed that one. If I won’t forget this by morning, will update the post with missing binding.
But I think after
docker-compose down
the volume still will be there. With random name, but there. I remember I had to explicitlydocker volume prune
to clean things up. So if that’s the case there’s still a chance to recover that data –docker volume ls
to list the volumes anddocker volume inspect
to find one whose mounting paths look mysql’y. Then you either could add missing volume mapping to docker-compose.yml, docker-compose up+down and move data from old volume to one that docker-compose just created, or evendocker volume create
with proper name and move data into it before very firstcompose up
. I pro-o-obably tried the first one.Thank you so much for this!
As a Docker newbie, this introduction really helped me understand a lot about how docker works.
Encountered no major issues!
Great write-up. Thanks!
Just wanted to point out that there’s another way to accomplish “Step 4. Fix web site address”
The
WP_HOME
andWP_SITEURL
constants can be defined in wp-config.php or wherever.Thank you, it’s good to know that
Hey pav, thanks for this writeup, it has been really helpful 🙂
Small remark:
$ mysqldump –add-drop-table -u **** -p **** | gzip > codeblog.dotsandbrackets.com.
date +%Y%m%d-%h%M%S
.sql.gz# Enter password:
# produces codeblog.dotsandbrackets.com.2070808-024302.sql.gz
Here you do not specify the name of the table wordpress is using. Running the command like this produces a gzipped man page of mysqldump. Could you please add a placeholder database name here?
Good point. I’ve updated the code snipped, thank you!
i tried this but data not inserting
hi,
Hope someone response. I try to do this, really good article, but I stuck on restore files. Firs, as I understand, author just get all wp files, archive it to tar.gz, but it isn’t use this archive in docker-compose.yml. And, I really don’t get why this archiwes is created and what I should to use there in volument part. What is ./codeblog.dotsandbrackets.com in
volumes:
– ./codeblog.dotsandbrackets.com:/var/www/html
Just folder with all wp files?
hey, yes, in the end it’s just a folder. I tarballed WordPress folder when taking a backup, and didn’t mention later in the post that it was untarred and stored next to
docker-compose.yml
, so it could be mounted to a container later. The only reason for archiving is performance – it’s one thing to download a tarball from a remote host, and the other – a folder with tons of small files.Thank you! Awesome, always wondered how to do that with an old WP deploy. What about SSL 😜? Maybe try to use the https://caddyserver.com container? It takes care of this automatically, and has one line support for fpm? If someone manages to do this please post. If I do first, will share too.
I actually ended up glueing findings from the post together into Vagrant file and deploying WP instances directly on cloud VMs. The reasoning was that I have several WPs, I want to keep them isolated on cheap hosts, and having Docker around them would be an overkill. From the top of my head I’d probably solve SSL by having a good old nginx as a reverse proxy with SSL in front of my docker host, but again – it’s an overkill for cheap hosts.
Hi. This is an excellent write-up. Do you know if it’s possible to work with different php versions using Docker? For example, if I wanted to test if my site will work with php 5.6, 7.4 and 8, how can I do that?
You can. In the examples above, I was using the latest version of php/fpm at that time –
image: wordpress:fpm
. However, there are more specific tags, e.g.wordpress:5.7-php8.0-fpm-alpine
. Just find the image tag with the version you’re looking for. Or check the sources of those images and make your own build – at the end of the day those images are backed with Dockerfiles with PHP being explicitly installed, so you can change them to whatever you want.Thank you very much! 🙂
My pleasure 🙂