Backup of a WordPress Site into a Docker container

WordPress is a publishing software used by many users. This website (http://www.opencloudblog.com) is using WordPress. If somebody has a running website with useful content, it’s a good practice to backup the data. And it is a even better practice to VERIFY that the data in a backup can be read and reused.

To verify the data, an installation of a LAMP (Linux, Apache, Mysql, PHP) stack is needed. The docker container framework is a good tool to implement this LAMP stack. In order to create a copy of a running WordPress site in a Docker container the following steps are necessary:

  • Create a simple LAMP Docker container, which contains everything. Expose the apache server via port 8080 and ssh via port 22
  • Copy the data from the WordPress site (mysql database and the webserver content)
  • Create a new Docker container, copy the backup data to this container and expose the webserver via http://127.0.0.1:8080 on the local host

Create a LAMP container

The LAMP Docker container is the base for the WordPress installation. The solution shown here is based on the work of https://github.com/tutumcloud/tutum-docker-lamp . The Dockerfile is modified and a ssh daemon is added to the container (I like SSH…). The database is also stored in the LAMP container to reduce the number external dependencies. The Dockerfile for the LAMP stack is:

# Main source: https://github.com/tutumcloud/tutum-docker-lamp
#
# docker build --no-cache --force-rm=true -t wp/lamp .
#
# VERSION               1.0.0

FROM     ubuntu:14.04
MAINTAINER Ralf Trezeciak

# make sure the package repository is up to date
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get -y upgrade

RUN DEBIAN_FRONTEND=noninteractive apt-get -y install openssh-server supervisor git apache2 libapache2-mod-php5 mysql-server php5-mysql pwgen php-apc

RUN apt-get clean

RUN mkdir /root/.ssh/
RUN mkdir /var/run/sshd
#
# insert your public ssh key here and remove the comment
#RUN echo "" > /root/.ssh/authorized_keys
RUN sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config 

# Add image configuration and scripts
ADD start-apache2.sh /start-apache2.sh
ADD start-mysqld.sh /start-mysqld.sh
ADD start-sshd.sh /start-sshd.sh
ADD run.sh /run.sh
RUN chmod 755 /*.sh
ADD my.cnf /etc/mysql/conf.d/my.cnf
ADD supervisord-apache2.conf /etc/supervisor/conf.d/supervisord-apache2.conf
ADD supervisord-mysqld.conf /etc/supervisor/conf.d/supervisord-mysqld.conf
ADD supervisord-sshd.conf /etc/supervisor/conf.d/supervisord-sshd.conf

# Remove pre-installed database
RUN rm -rf /var/lib/mysql/*

# Add MySQL utils
ADD create_mysql_admin_user.sh /create_mysql_admin_user.sh
ADD import_sql.sh /import_sql.sh
ADD create_db.sh /create_db.sh
RUN chmod 755 /*.sh

# init the databases
RUN mysql_install_db --user=mysql 

# config to enable .htaccess
ADD apache_default /etc/apache2/sites-available/000-default.conf
RUN a2enmod rewrite

# Configure /app folder with sample app
RUN git clone https://github.com/fermayo/hello-world-lamp.git /app
RUN mkdir -p /app && rm -fr /var/www/html && ln -s /app /var/www/html

EXPOSE 22 8080 
CMD ["/run.sh"]

The other files needed in the docker directory are contained the following tar.gz file: Additional LAMP files

Now it’s time to build the LAMP Docker container using the command

docker build --no-cache --force-rm=true -t wp/lamp .

Copy WordPress data

To copy the wordpress data requires two steps. The first step is to copy the directory containing the php wordpress code and the static content (e.g. the media files). The following code (works for Strato WordPress installations)

# assume, that rsync can be used via ssh, copy the wordpress static data
#
rsync -av -e ssh <user>@<yoursite>:<your wordpress directory> ./data/

copies the static content to the local directory ./data/ .

Now the data of the mysql database is required. Your provider might offer a web/php backup tool, but I prefer a scripted solution, which works for Strato WordPress installations. Strato creates backups of the mysql database once per hour. The following script copies the last version of the backup to a local file:

#!/bin/bash
#
USERID='<the user id of your database>'
DBNAME='<the name of the database>'
PASSWORD='<the password of your database>'
#
SSH='ssh -n <your login>@ssh.strato.de '
SCP="scp -p <your login>@ssh.strato.de"
#
# get the name of the last backup
#
LASTDB=$($SSH mysqlbackups ${USERID} | head -1)
#
DATUM=$(date +%s)
#
DUMPFILE="wp-database-backup.sql"
#
# make a copy of the database backup
#
$SSH "mysqldump --add-drop-table -h ${LASTDB} -u ${USERID} -p${PASSWORD} ${DBNAME} > ${DUMPFILE}"
XDIR=.
X=$(${SCP}:${XDIR}/${DUMPFILE} .)
#

Create the directory to for the WordPress container

Now you must create the directory for the WordPress docker container data. This directory contains the docker manifest and the static data.

Post process the WordPress data

After you have downloaded the WordPress data from your WordPress site, it is necessary to postprocess the data.

  • Create a tar gz file of the static content
  • Change the WordPress „site url“ in the mysql backup to 127.0.0.1:8080
  • Get the wp-config.php file from the static data and change the parameters for the mysql database

The compression of the static data is done by the following script:

#!/bin/bash
#
cd <path to static data>
# fix the permissions
find . -type d -exec chmod 0755 {} \;
find . -type f -exec chmod 0644 {} \;
# create a tar.gz file in the WP container directory
tar -clSzpf wordpress.tar.gz ./

Changing the WordPress site URL is done using the following script:

#!/bin/bash
cd <path to static data>
# change the sed command and replace www.opencloudblog.com by the url of your site 
cat wp-database-backup.sql | sed 's#www\.opencloudblog\.com#127.0.0.1:8080#g' > wordpress.sql 
python <wp docker directory>/sfix.py wordpress.sql

The used solution to do this is very dirty!!! I used this, because I did not find any simple solution to do this using WordPress. After running sed on the sql backup file, the database needs to by fixed, because string lengths might change (keyword php serialization). To do this, I use the python helper script sfix.pl from https://gist.github.com/astockwell/6489489:

#!/usr/bin/env python
#
# Source: https://gist.github.com/astockwell/6489489

import os, re

# Regexp to match a PHP serialized string's signature
serialized_token = re.compile(r"s:(\d+)(:\\?\")(.*?)(\\?\";)")

# Raw PHP escape sequences
escape_sequences = (r'\n', r'\r', r'\t', r'\v', r'\"', r'\.')

# Return the serialized string with the corrected string length
def _fix_serialization_instance(matches):
  target_str = matches.group(3)
  ts_len = len(target_str)
  
  # PHP Serialization counts escape sequences as 1 character, so subtract 1 for each escape sequence found
  esc_seq_count = 0
  for sequence in escape_sequences:
      esc_seq_count += target_str.count(sequence)
  ts_len -= esc_seq_count
  
  output = 's:{0}{1}{2}{3}'.format(ts_len, matches.group(2), target_str, matches.group(4))
  return output
    
# Accepts a file or a string
# Iterate over a file in memory-safe way to correct all instances of serialized strings (dumb replacement)
def fix_serialization(file):
  try:
    with open(file,'r') as s:
      d = open(file + "~",'w')
      
      for line in s:
        line = re.sub(serialized_token, _fix_serialization_instance, line)
        d.write(line)
        
      d.close()
      s.close()
      os.remove(file)
      os.rename(file+'~',file)
      print "file serialized"
      return True
  except:
    # Force python to see escape sequences as part of a raw string (NOTE: Python V3 uses `unicode-escape` instead)
    raw_file = file.encode('string-escape')
    
    # Simple input test to see if the user is trying to pass a string directly
    if isinstance(file,str) and re.search(serialized_token, raw_file):
      output = re.sub(serialized_token, _fix_serialization_instance, raw_file)
      print output
      print "string serialized"
      return output
    else:
      print "Error Occurred: Not a valid input?"
      exit()

if __name__ == "__main__":
  import sys
  
  try:
    fix_serialization(sys.argv[1])
  except:
    print "No File specified, use `python serialize_fix.py [filename]`"

Place sfix.py in the wp container directory.

Do not forget to save a copy of the wp-config.php file to the wp container directory and set the following values to:

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'wordpress');
/** MySQL database username */
define('DB_USER', 'root');
/** MySQL database password */
define('DB_PASSWORD', '');
/** MySQL hostname */
define('DB_HOST', 'localhost');

The WordPress Docker Container

The wordpress docker container is build using the following Dockerfile:

#
# docker build --no-cache --force-rm=true -t wp/blog .
#
# docker run -d -p 8080:8080 -p 8022:22 --name=oc wp/blog
#
FROM wp/lamp:latest
MAINTAINER RTR

# Configure WordPress to connect to local DB
ADD wp-config.php /app/wp-config.php

# save the original config
RUN mv -v /app/wp-config.php /app/wp-config.php.org

# copy the wordpress stuff from strato
ADD wordpress.tar.gz /app/

# save the strato config
RUN mv -v /app/wp-config.php /app/wp-config.php.strato

# restore the original config
RUN cp -v /app/wp-config.php.org /app/wp-config.php

# add the owner of the content and change the owner and Modify permissions to allow plugin upload
RUN useradd -d /var/www -g www-data -M -s /usr/sbin/nologin -c "Owner of the content" www-owner &&\
    chown -R www-data:www-data /app &&\
    find /app -type d -exec chmod 775 {} \; 

# Modify permissions to allow plugin upload
# RUN chmod -R 777 /app/wp-content

# copy the database backup
ADD wordpress.sql /root/

# copy the db import script
ADD import-db.sh /root/
  
RUN  bash /root/import-db.sh

# apache listens to 8080
RUN echo "Listen 8080" > /etc/apache2/ports.conf

EXPOSE 22 8080
CMD ["/run.sh"]

The Dockerfile requires a helper script to import the database:

#!/bin/bash

/usr/bin/mysqld_safe > /dev/null 2>&1 &

RET=1
while [[ RET -ne 0 ]]; do
    echo "=> Waiting for confirmation of MySQL service startup"
    sleep 5
    mysql -uroot -e "status" > /dev/null 2>&1
    RET=$?
done

mysql -u root -e "create database wordpress;"
mysql -u root wordpress < /root/wordpress.sql
ls -la /var/lib/mysql/

mysqladmin -uroot shutdown

To build the image, you need to run

docker build --no-cache --force-rm=true -t wp/blog .

Check with the command docker images, that the image has been created:

# docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
wp/blog                latest              4fffcac92b7d        9 seconds ago       677 MB
ubuntu                 14.04               accae238329d        3 weeks ago         221.1 MB
wp/lamp                latest              932e7a77f9dd        2 hours ago         518.2 MB

Run the container

Now it’s time to run your copy of your wordpress site. Create a container using the command:

docker run -d -p 8080:8080 -p 8022:22 --name=wordpresscopy wp/blog
# check if the container is running
docker ps

Connect to http://127.0.0.1:8080 to connect to the webserver and access the local copy of the wordpress site.Now you may run updates of wordpress, plugins, themes,… in the container.  If the versions of PHP, apache, mysql match the version of your original site, you can use the docker copy of your WordPress site as a test system.

You can login to the container using ssh to port 8022 (ssh -l root -p 8022 127.0.0.1)

CONTAINER ID   IMAGE            COMMAND    CREATED         STATUS          PORTS                                        NAMES
81595c6da3c2   wp/blog:latest   "/run.sh"  29 seconds ago  Up 28 seconds   0.0.0.0:8022->22/tcp, 0.0.0.0:8080->8080/tcp wordpresscopy

You can create an image from your started container to create an archive of snapshots, holding e.g. monthly copies of the original content to review changes:

#
# stop the conatiner
docker stop wordpresscopy
#
# get the ID of the created docker container
ID=$(docker inspect --format="{{ .Id }}" wordpresscopy)
#
# create an image from a container
docker commit ${ID} wordpress/archive-2014-11
#
# list all images
docker images
Updated: 17/01/2021 — 13:17