That title is quite the mouthful but it has to be as this particular set up has a couple of gotchas in it. I assume that you have Docker installed and that the firewall is correctly configured as detailed in this earlier guide. I can’t stress enough, you have to have proved that the firewall is working and allowing packets though before you continue. I’m moving from a cPanel webhost to self hosting so the first section of this article may not be relevant to you if you are making a fresh install.
I’ll be setting up Nginx, MariaDB and WordPress in this build. I’ll only show the configuration of a single WordPress container but if you want more you just duplicate the settings in the WordPress compose file. The setup I will use will involve three compose files for the three different services. I’ve asked the internet and there doesn’t seem to be a consensus about how to arrange services. If anything there was a slight lean towards a single compose file for everything but I wanted them separate. The way I configure Docker containers seems to be slightly unusual, it was the way I learnt and it works for me, at some point I should probably update my knowledge.
All the docker setup takes place in a the directory ~/docker
with a subdirectory for each service stack.
Understanding the cPanel Hosting Backup
The reason I’m setting up WordPress on my own server is because my web host of many years has decided to stop providing simple web hosting, they did have a nice range of VPS’s though so I grabbed one of them instead. I have never really bothered to explore cPanel so forgive me if some of this is overly simple (or possibly even wrong).
cPanel supports hosting a single site and then a number of what it calls addons. The primary site is held under ~/public_html
(where ~/
is home as displayed in the cPanel file manager). The images and other static content for the site are under ~/public_html/wp-content/
. Additional sites are found in /addons
with a separate directory for each additional site.
When you perform a full backup of a cPanel site you don’t get just the home you see in the file manager you actually get one level above that. When working with the backup later you need to look in homedir to find the files mentioned above. For mysql backups you need to look in /mysql
(where / here is the root of the backup), there will be a .sql and .create file for each database you have.
Docker Service Arrangement
As I mentioned in the introduction I’m going to go with what I think is a somewhat unusual configuration for my containers. I have a resource constrained server on to which I’ll be deploying four WordPress instances, MariaDB, and Nginx. Common wisdom seems to be to have a single compose that deploys all of this in one go but I don’t want that. The Nginx server will be a reverse proxy and will almost certainly be acting on behalf of more than just the WordPress sites in the future. For that reason, in my mind at least, it should be in it’s own compose file. The MariaDB database is more tightly coupled to WordPress instances but is also likely to be used by other things so, again, in my mind it should be independent. This is where I differ with the norm though, I think. I’m using Docker as if it’s light weight virtualisation and it seems I should be thinking of it as a number of self contained application stacks. What I’m doing works but I can see the benefit of keeping things separate. The upside of what I’m doing means there is only a single database to worry about. All the WordPress sites will be in a single compose file.
Copy the Backup Files to the Server
Before you start the restore process copy the site files from your local machine up to the server. On the server create the directory ~/restore
and then cd
into it. On your home machine issue an scp command to copy the backup data to the server (I’m assuming you have the backed up cPanel data uncompressed on your home machine). For example:
$ scp -r example_site/ your_server.example.com:~/restore/
MariaDB
Installation and Configuration
Create the directory ~/docker/mariadb
and in there a compose.yaml
file. In the compose file add the following content. Notice that I’ve set up MariaDB so that it only listens on the container subnet gateway, it doesn’t publish ports to all interfaces. This port setting alone ensures that MariaDB isn’t published to the world but you should also make sure that your nftables rules are such that port 3306 is blocked (or at least very locked down).
services: mariadb: image: mariadb:latest container_name: mariadb environment: - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD} volumes: - ${MARIADB_DATASTORE}:/var/lib/mysql ports: - 10.10.0.1:3306:3306 restart: unless-stopped networks: web: ipv4_address: 10.10.0.3 networks: web: external: true
Also create a .env
file and add the two environment variables:
MARIADB_ROOT_PASSWORD=secret MARIADB_DATASTORE=/data/mariadb/data
Finally create the directory ~/docker/mariadb/data
which will hold the actual database files. That’s all there is to it.
Restoring a WordPress Database
There are loads of different ways to do this but the quickest and simplest is the command line. Begin by installing the MaridDB client on the host machine.
sudo apt install mariadb-client
The client utilities give you access to the MariaDB command line. Open a connection to the server with a command like the one shown below. For the host you probably want to use the IP address of the gateway you specified in the compose file.
mariadb --user username --password=secure_password --host mariadb.host
The cPanel host I was using ran a MySQL system and I’m switching to MariaDB. The two are almost completely compatible but I found the create script MySQL created in the backup didn’t work in MariaDB. The solution is to just create the database manually, it’s a single SQL instruction like this:
CREATE DATABASE `wp_example`;
This will create a database with utf8mb4 as the default character set which is not what the old MySQL database was using but it should be fine. The database doesn’t have to have a wp_ prefix but almost all hosting providers do this and I think it’s fairly useful.
To restore the data you first need to use the correct database:
use wp_example;
Now issue a source instruction and give it the full path your your .sql
backup file, an example is shown below. The import of the data will be complete in no time.
source /home/username/restore/example/backup_file_example.sql;
I use a separate database user for each WordPress instance as that allows me to lock down the user to just the one database it should have access to. Creating this user and granting it all rights to the database just restored is most easily done using a GUI database management tool like DBeaver (or phpMyAdmin if you must). If you use something like DBeaver you’ll probably need to open a hole though the firewall or set up an SSH tunnel. That’s beyond this article but remember to lock everything down tight once you’re finished.
You might now want to check out the additional setup section later on in this article if you have non-standard table prefixes or anything like that.
WordPress
Create the Folder Structure
Create the following folder structure where sitename is the name of the site that you are restoring.
~/docker wordpress compose.yaml .env sitename
The content for the WordPress site will be placed in the sitename folder but for this install method that will be only the wp-content folder from the backup. All the other files for the WordPress site will come from the container. This allows the site to be updated by updating the container, exactly what we want to happen.
Copy in the Existing Content
All you need to do is copy the wp-content folder from the backup into the sitename folder mentioned above. For example:
cd ~/docker/wordpress/sitename/ cp -r ~/restore/sitename/sitename.com/wp-content/ .
You now need to make sure the wp-content
directory and everything under it is owned by www-data:www-data
otherwise the WordPress container won’t be able to manipulate it (e.g. you won’t be able to upload new images). Note that the site might mostly work at the moment as the files in wp-content are probably readable by all. From the ~/docker/wordpress/sitename/
directory:
sudo chown -R www-data:www-data wp-content/
I should note at this point that not every Linux distribution comes with the user www-data (with user-id 33). If that’s the case you should probably read this guide which produces a similar set up to this but moves the WordPress container to a host user that does exist.
Docker Configuration
Create the compose file mentioned above and populate it with the following settings.
services: wordpress-sitename: container_name: wordpress-sitename hostname: www.sitename.com image: wordpress:latest restart: unless-stopped environment: - WORDPRESS_DB_HOST=$WORDPRESS_DB_HOST - WORDPRESS_DB_NAME=$SITENAME - WORDPRESS_DB_USER=$SITENAME_DB_USER - WORDPRESS_DB_PASSWORD=$SITENAME_DB_PASSWORD volumes: - ./sitename/wp-content:/var/www/html/wp-content networks: web: ipv4_address: 10.10.0.13 networks: web: external: true
In the .env file you want the entries shown below.
WORDPRESS_DB_HOST=10.10.0.3:3306 SITENAME_DB_NAME=wp_sitename SITENAME_DB_USER=wp_sitename SITENAME_DB_PASSWORD=secret
There is nothing particularly exciting in the settings. The use of environment variables causes WordPress to switch the wp-config.php
file it uses to one that accepts environment variables. The setting of the host name is not mandatory but it does stop Apache from complaining about not knowing what the host name is – I thought it would stop a redirect issue I was having. Then there’s the mapping of the local wp-content
directory into the container. This makes backing up and moving the WordPress install very simple, you just need that folder and a database dump.
The more important settings are made in the server settings in Nginx. In particular in the location where Nginx executes the proxy pass you need to have the following additional statements:
proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off;
If you fail to include these additional settings the site won’t work correctly, and the failures can be many and varied. In my case attempting to navigate to the wp-admin
page caused a redirect to the IP of the WordPress container (of all places). I don’t fully understand why these settings are needed, it’s something to do with the fact Nginx doesn’t pass headers on when reverse proxying, it is discussed more here.
Check the additional setup section in case there is anything there that you need to do and then you’re ready to start the site with sudo docker compose up
. Note that on the first execution WordPress will probably install the two default plugins, no idea why this happens but they can simply be deleted.
Additional Setup
Removing LiteSpeed Cache
LiteSpeed Cache is a caching plugin that was needed on my old host to get any sort of performance out of the site. Now that I’m hosting on a decent server with Cloudflare CDN I don’t need this any more. Normally, you can just go to the plugins page and delete it but for some reason that causes an FTP connection window to pop open. I’ve never used FTP with my site so I’m not sure where that comes from but to manually remove the plugin it’s easiest to just delete the plugin folder in wp-content/plugins
. Additionally, you’ll want to delete to two Litespeed tables from the database. Litespeed also seems to fill your wp_options
table full of settings, I just left them in place, presumably they could be deleted.
Changing the Table Prefix
By default WordPress expects all it’s tables to be prefixed with wp_
, this is so that hosting providers can run a single database per-customer with multiple WordPress sites – you just change the prefix per site. One of the sites I run had a non-standard prefix when it was cPanel hosted. To deal with that you can either specify the environment variable WORDPRESS_TABLE_PREFIX in the compose or just rename all the tables to have a wp_
prefix – I want to go with the latter to make all my installs the same. The most obvious symptom of the prefix being wrong is WordPress trying to reinstall itself when you’ve already installed it.
To rename the tables just open the database in DBeaver and edit the tables, right click on the table and select rename. It seems phpMyAdmin has a specific tool for this job. It’s tedious but only needs doing once.
You also need to run a handful of queries to update the database as the prefix is stored in some tables. See here for more details.
update NEWPREFIX_usermeta set meta_key = 'NEWPREFIX_capabilities' where meta_key = 'OLDPREFIX_capabilities'; update NEWPREFIX_usermeta set meta_key = 'NEWPREFIX_user_level' where meta_key = 'OLDPREFIX_user_level'; update NEWPREFIX_usermeta set meta_key = 'NEWPREFIX_autosave_draft_ids' where meta_key = 'OLDPREFIX_autosave_draft_ids'; update NEWPREFIX_options set option_name = 'NEWPREFIX_user_roles' where option_name = 'OLDPREFIX_user_roles';
Updating the Containers
WordPress, in particular, needs to be kept up to date. Using Docker this is very easy:
sudo docker compose pull && sudo docker compose up -d
Note: having run this setup for a few months it has become apparent that WordPress does not play nicely with Docker. The WordPress image expects to be unpacked entirely and then update itself. I honestly don’t know why they bothered to package it for Docker. Anyway, as long as you are willing to prod it a bit it’s possible to make it work like most Docker image. In particular you might find the WordPress appears to not update itself when you pull a new version. This seems to be related to database updates. If you docker compose down
followed by docker compose up -d
and then log back into WordPress it seems to offer the database update and start working again.