In this article, we take a look at how to migrate a WordPress installation from a normal installation into a container. In case you missed it, in a previous article we gave a rundown of the general process we intend to use.
Containerizing WordPress seems so deceptively easy. It's a standard LAMP stack, but there are a few pitfalls we want to avoid. Containers are really two things: Container images at rest and Linux processes at runtime. Let's take a look at both parts—build and run.
Editor's Note: For the purpose of this article, we assume you'll be building your containers on Red Hat Enterprise Linux 8 using podman build. You may be able to use the instructions on other distributions or with other toolchains, however, some modifications may be required.
WordPress needs PHP and a web server. The most common configuration is to use Apache (or Nginx) with PHP FastCGI Process Manager (php-fpm) and a PHP interpreter. In fact, a general-purpose container image can be constructed for almost any PHP-based application, including WordPress and MediaWiki. Here's an example of how to build one with Red Hat Universal Base Image:
FROM registry.access.redhat.com/ubi8/ubi-init MAINTAINER fatherlinux <email@example.com> RUN yum install -y mariadb-server mariadb php php-apcu php-intl php-mbstring php-xml php-json php-mysqlnd crontabs cronie iputils net-tools;yum clean all RUN systemctl enable mariadb RUN systemctl enable httpd RUN systemctl disable systemd-update-utmp.service ENTRYPOINT ["/sbin/init"] CMD ["/sbin/init"]
ubi-init image is configured out of the box to run
systemd in the container when run. This makes it easy to run a few commands at install and rely on the subject matter expertise embedded in the Linux distribution. As I've argued for years, the quality of the container image and supply chain hygiene are more important than the absolute smallest individual images we can produce (Container Tidbits: Can Good Supply Chain Hygiene Mitigate Base Image Sizes?). We need to consider your supply chain's total size, not the individual images, so I chose the
Notice how simple the Containerfile is. That's because we are relying on the packagers to start the services correctly. See also: Do Linux Distributions Still Matter with Containers?
It's a fairly simple build, so let's move on to the tricky stuff at runtime.
[ You might also like: Moving from docker-compose to Podman pods ]
Like traditional services, on traditional servers, running our containers with
systemd gives us a convenient way to start them when we boot our container host or when the container is killed (recoverability in the table above). Let's dissect our systemd unit file to better understand the design decisions and some of the advantages of running services in containers:
[Unit] Description=Podman container - wordpress.crunchtools.com [Service] Type=simple ExecStart=/usr/bin/podman run -i --read-only --rm -p 80:80 --name wordpress.crunchtools.com \ -v /srv/wordpress.crunchtools.com/code/wordpress:/var/www/html/wordpress.crunchtools.com:Z \ -v /srv/wordpress.crunchtools.com/config/wp-config.php:/var/www/html/wordpress.crunchtools.com/wp-config.php:ro \ -v /srv/wordpress.crunchtools.com/config/wordpress.crunchtools.com.conf:/etc/httpd/conf.d/wordpress.crunchtools.com.conf:ro \ -v /srv/wordpress.crunchtools.com/data/wp-content:/var/www/html/wordpress.crunchtools.com/wp-content:Z \ -v /srv/wordpress.crunchtools.com/data/logs/httpd:/var/log/httpd:Z \ -v /srv/wordpress.crunchtools.com/data/mariadb/:/var/lib/mysql:Z \ --tmpfs /etc \ --tmpfs /var/log/ \ --tmpfs /var/tmp \ localhost/httpd-php ExecStop=/usr/bin/podman stop -t 3 wordpress.crunchtools.com ExecStopAfter=/usr/bin/podman rm -f wordpress.crunchtools.com Restart=always [Install] WantedBy=multi-user.target
First, notice that we are running this entire container as read-only and with the
–rm option, making it ephemeral. The container is deleted every time it is stopped. This forces us to split up our code, configuration, and data and save it on external mounts, or it will be lost. This also gives us the ability to kill the container to pick up config file changes like a normal service (more on this later). Apache, PHP FPM, and MariaDB run side by side in the container, conveniently allowing them to communicate over private sockets. For such a simple service, there is no need to scale MariaDB and Apache separately, so there's no need to split them up.
Notice that we split the code, configuration, and data into separate directories and bind mounts. The main Apache, PHP, and PHP FPM binaries come from the
httpd-php container image built on Red Hat Universal Base Image, while the WordPress code comes from the code/wordpress bind mount. In many containers, all of the code will come from the container image (see Request Tracker later). The code/wordpress directory just houses WordPress PHP code downloaded from wordpress.org. None of our personal data or customizations are saved in the code/wordpress directory. Still, we purposefully made it a separate, writable bind mount to allow WordPress to auto-update itself at runtime. This is contrary to typical best practices with containers but a very convenient feature for a popular public-facing web service that is under constant attack and receives security updates frequently. Architecting it this way gives us over the air updates without having to rebuild the container image. Making services as driverless as possible is definitely useful.
Now, look at the config lines. Every customized config file is bind-mounted into the container read-only. This is a solid security upgrade from traditional LAMP servers (virtual machines or bare metal). This prevents the usage of some WordPress plugins which try to change wp-config.php, but most sysadmins would want to disable these anyway. This could be made read-write if some of our users really need these plugins.
Next, notice the data directory. We bind mount three different subdirectories. All of them are writable:
- data/wp-content – This directory has our personal data and customizations in it. This includes things like WordPress themes, plugins, and uploaded files (images, videos, mp3s, etc.). It should also be noted that this is a WordPress Multi-User (MU) site, so multiple sites save their data here. A WordPress Administrator could log-in and create new sites if necessary.
- data/logs – We want our Apache logs outside the container so that we can track down access/errors or do analytics. We could also use these should somebody hack in, and we need to reconstruct what happened. A write-only mount option might be useful here.
- data/mariadb – This is our writable directory for MariaDB. Most of our secrets are stored in the database, and this directory has permissions set correctly for the mysql user/group. This gives us equivalent process-level isolation in the container, similar to a normal LAMP server. There is a bit of a security upgrade because this MariaDB instance only has WordPress data in it. Hackers can't break into WordPress and get to our Wiki nor Request Tracker, which have their own separate MariaDB instances.
Next, let's take a look at the
–tempfs mounts. These enable
systemd to run properly in a read-only container. Any data written to these mounts will be automatically deleted when the container stops. This makes everything outside of our bind mounts completely ephemeral. Other modifications could be made to capture
/var/log/messages or other logs if desired.
For backups within WordPress, we rely on UpdraftPlus. UpdraftPlus offers the advantage of backing up everything from a WordPress MU site, including themes, plugins, files, and the database. It can even push the backup to remote storage like Dropbox or pCloud (through WebDav). This is a common design pattern with higher-level applications like WordPress. Often, databases, CRMs, etc. will have their own backup utilities or ecosystems of third-party backup software. Relying on this existing software is still useful in containers.
[ Getting started with containers? Check out this free course. Deploying containerized applications: A technical overview. ]
It took me a very long time to finally get these applications containerized, but the effort paid off. It's good to think about this sort of project in terms of easy/moderate/difficult ratings. It's also useful at least consider lift-and-shift, refactoring, and rewriting. As you can see, these migrations can require a great deal of effort. That's a big part of why planning is so important.
I emphasized some good security and performance practices, as well. I also stuck to the Unix tenets of modularity with the separation of code, configuration, and data.
Now that we've moved WordPress, it's time to up the ante a bit. The next article in this series looks at containerizing MediaWiki. Once you've had a chance to digest that one, we'll take a swing at Request Tracker.
This series is based on "A Hacker's Guide to Moving Linux Services into Containers" on CrunchTools.com and is republished with permission.