One of the largest foundations for websites today is wordpress, with its ever increasing set of features and usability improvements. But while using the wordpress interface itself has become incredibly easy, most users are still locked into preconfigured hosting services - but with the rise of container technologies, hosting and maintaining your own wordpress site has become easier than ever.
Preparing the system
We are assuming you have a linux distribution installed on your server, with docker installed (see here for instructions). We also assume that you are running commands as root
for simplicity; if not, adjust by prepending sudo
before scripts as needed. Now, we create a new installation location for our wordpress setup:
mkdir -p /opt/wordpress/backups
cd /opt/wordpress
Now, create the main docker compose file:
docker-compose.yml
services:
wordpress:
image: wordpress:6
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: ${DB_USER:?You must set a value for DB_USER in the .env file!}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD:?You must set a value for DB_PASSWORD in the .env file!}
WORDPRESS_DB_NAME: ${DB_NAME:?You must set a value for DB_NAME in the .env file!}
volumes:
- $PWD/html:/var/www/html
labels:
- "traefik.enable=true"
- "traefik.http.routers.wordpress.rule=Host(`${DOMAIN:?You must set a value for DOMAIN in the .env file!}`)"
- "traefik.http.routers.wordpress.entrypoints=websecure"
- "traefik.http.routers.wordpress.tls.certresolver=letsencrypt"
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?You must set a value for DB_ROOT_PASSWORD in the .env file!}
MARIADB_DATABASE: ${DB_NAME:?You must set a value for DB_NAME in the .env file!}
MARIADB_USER: ${DB_USER:?You must set a value for DB_USER in the .env file!}
MARIADB_PASSWORD: ${DB_PASSWORD:?You must set a value for DB_PASSWORD in the .env file!}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-u${DB_USER}", "-p${DB_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
reverse-proxy:
image: traefik:v2
restart: always
command:
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=${EMAIL:?You must set a value for EMAIL in the .env file!}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "letsencrypt:/letsencrypt"
labels:
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
volumes:
db_data:
letsencrypt:
This seems like a lot, but it's rather plain for a wordpress installation. It consists of 3 services: a wordpress container, a mariadb (mysql) database, and a traefik reverse proxy (to handle automatic SSL certificates).
The compose file already contains all the wiring needed, so all you need to do is create a tiny configuration file:
.env
DOMAIN=example.com
EMAIL=mail@example.com
DB_ROOT_PASSWORD=secureadminpassword
DB_USER=wordpress
DB_PASSWORD=secretpassword
DB_NAME=wordpress
You need to change the values of these variables to match your desired website domain and email address (to register with letsencrypt for free SSL certificates).
With these two files in place, all you need to do is start the containers:
docker-compose up -d
Wait until all containers are started, then open the domain in your browser. You might get a warning about the SSL certificate being invalid, which should resolve itself after a few seconds.
Making backups
One of the most important maintenance tasks for production-level hosting is creating backups.
In order to make this step as easy as possible, we will write a quick script for this task as well. Make sure to save this in the same directory as the docker-compose.yml
file!
create_backup.sh
#!/bin/bash
set -e
source "$PWD/.env"
TEMPDIR=$(mktemp -d)
trap 'rm -r "$TEMPDIR"' EXIT
DATE=$(date +%c | sed 's/ /_/g; s/:/-/g')
echo "Ensuring containers are running..."
docker-compose up -d
echo "Backing up database..."
docker-compose exec db bash -c "mkdir -p /backups && mariadb-dump -u$DB_USER -p$DB_PASSWORD $DB_NAME" > "$TEMPDIR/db.sql"
echo "Backing up website files..."
cp -r "$PWD/html" "$TEMPDIR"
tar -czf "$PWD/backups/backup_$DATE.tar.gz" -C "$TEMPDIR" .
echo "Backup successfully created at backups/backup_$DATE.tar.gz"
The script will make backups of both the wordpress files (including themes, uploads etc) and the database contents. Backups are saved to the backups/
directory, and their contents are standardized so you can easily use them to move your website to a different server or paid hoster later if you like.
The last thing to do is give execution permissions to the script:
chmod +x create_backup.sh
To make a backup, simply run the script:
./create_backup.sh
The script will tell you what step it is processing and print the full location of the created backup archive at the end.
Automating scheduled backups
Now backups are important, but they are only useful if you have a reasonably recent backup available when you need one. Manually making backups every day isn't a great idea, so we will configure the server to automatically run the backup script every night.
Append an automatic execution job of the backup script:
(crontab -l 2>/dev/null; echo "0 2 * * * cd $PWD && ./create_backup.sh" && find $PWD/backups/ -name 'backup_*.tar.gz' -mtime +7 -delete) | crontab -
Make sure to run this command as root
and from the same directory as the docker-compose.yml
file!
The scheduled job runs every night at 2am, and also deletes backups older than 7 days to prevent the disk from filling up with old and unused backups.
Restoring from a backup
In order to get any use out of the backups we made earlier, we need a way to reliably restore them too. Keeping with the simplicity of automation, this can be turned into a bash script as well:
restore_backup.sh
#!/bin/bash
set -e
source "$PWD/.env"
[ "$#" -eq 1 ] || { echo "Usage: $0 <backup-file>"; exit 1; }
BACKUP_FILE="$1"
[ -f "$BACKUP_FILE" ] || { echo "Backup file '$BACKUP_FILE' does not exist."; exit 1; }
TEMPDIR=$(mktemp -d)
trap 'rm -r "$TEMPDIR"' EXIT
echo "Extracting backup..."
tar -xzf "$BACKUP_FILE" -C "$TEMPDIR"
echo "Removing old containers..."
docker compose down -v
echo "Restoring website files..."
rm -rf "$PWD/html"
cp -r "$TEMPDIR/html" "$PWD/html"
echo "Starting clean containers..."
docker compose up -d
echo "Waiting 5s for database to initialize..."
sleep 5
echo "Restoring database..."
docker compose exec -T db bash -c "mariadb -h localhost -u$DB_USER -p$DB_PASSWORD $DB_NAME" < "$TEMPDIR/db.sql"
echo "Backup restored!"
Don't forget to make the script executable:
chmod +x restore_backup.sh
Now all you need to do if you want ot restore a backup, you simply execute the script and pass it the backup archive file you want to recover:
./restore_backup backups/backup_Fri_18_Apr_2025_03-08-31_PM_CEST.tar.gz
This would then delete all currently running versions of the wordpress database and files and restore the contents of the backup archive backup_Fri_18_Apr_2025_03-08-31_PM_CEST.tar.gz
, complete including a restart of all necessary containers.
After a few seconds, the wordpress page should be up and running again, with the state of the backup.
Updating wordpress
Since the wordpress installation is using container images, all you need to do in order to update is to pull the new image and run it:
docker-compose pull docker-compose up -d
This will update to the latest version of the selected wordpress release. If you want to jump between releases, you should carefully read the release notes and migration guide for the new version, then find the line containing the wordpress image version in the file docker-compose.yml
, for example:
image: wordpress:6
Change the last number to the wordpress release you want to update to, then run the docker-compose commands from above.
Remember to make a backup before upgrading wordpress or your database!