{"id":511,"date":"2014-11-16T19:06:40","date_gmt":"2014-11-16T18:06:40","guid":{"rendered":"http:\/\/www.opencloudblog.com\/?p=511"},"modified":"2021-01-17T13:17:48","modified_gmt":"2021-01-17T12:17:48","slug":"backup-of-a-wordpress-site-into-a-docker-container","status":"publish","type":"post","link":"https:\/\/www.opencloudblog.com\/?p=511","title":{"rendered":"Backup of a WordPress Site into a Docker container"},"content":{"rendered":"<p>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&#8217;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.<\/p>\n<p>To verify the data, an installation of a LAMP (<strong>L<\/strong>inux, <strong>A<\/strong>pache, <strong>M<\/strong>ysql, <strong>P<\/strong>HP) 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:<\/p>\n<ul>\n<li>Create a simple LAMP Docker container, which contains everything. Expose the apache server via port 8080 and ssh via port 22<\/li>\n<li>Copy the data from the WordPress site (mysql database and the webserver content)<\/li>\n<li>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<\/li>\n<\/ul>\n<h2>Create a LAMP container<\/h2>\n<p>The LAMP Docker container is the base for the WordPress installation. The solution shown here is based on the work of <a title=\"Tutam LAMP Stack\" href=\"https:\/\/github.com\/tutumcloud\/tutum-docker-lamp\">https:\/\/github.com\/tutumcloud\/tutum-docker-lamp<\/a> . The Dockerfile is modified and a ssh daemon is added to the container (I like SSH&#8230;). The database is also stored in the LAMP container to reduce the number external dependencies. The Dockerfile for the LAMP stack is:<\/p>\n<pre class=\"lang:sh decode:true\" title=\"The Dockerfile\"># Main source: https:\/\/github.com\/tutumcloud\/tutum-docker-lamp\r\n#\r\n# docker build --no-cache --force-rm=true -t wp\/lamp .\r\n#\r\n# VERSION               1.0.0\r\n\r\nFROM     ubuntu:14.04\r\nMAINTAINER Ralf Trezeciak\r\n\r\n# make sure the package repository is up to date\r\nRUN apt-get update\r\nRUN DEBIAN_FRONTEND=noninteractive apt-get -y upgrade\r\n\r\nRUN DEBIAN_FRONTEND=noninteractive apt-get -y install openssh-server supervisor git apache2 libapache2-mod-php5 mysql-server php5-mysql pwgen php-apc\r\n\r\nRUN apt-get clean\r\n\r\nRUN mkdir \/root\/.ssh\/\r\nRUN mkdir \/var\/run\/sshd\r\n#\r\n# insert your public ssh key here and remove the comment\r\n#RUN echo \"\" &gt; \/root\/.ssh\/authorized_keys\r\nRUN sed -i 's\/^PermitRootLogin.*\/PermitRootLogin yes\/' \/etc\/ssh\/sshd_config \r\n\r\n# Add image configuration and scripts\r\nADD start-apache2.sh \/start-apache2.sh\r\nADD start-mysqld.sh \/start-mysqld.sh\r\nADD start-sshd.sh \/start-sshd.sh\r\nADD run.sh \/run.sh\r\nRUN chmod 755 \/*.sh\r\nADD my.cnf \/etc\/mysql\/conf.d\/my.cnf\r\nADD supervisord-apache2.conf \/etc\/supervisor\/conf.d\/supervisord-apache2.conf\r\nADD supervisord-mysqld.conf \/etc\/supervisor\/conf.d\/supervisord-mysqld.conf\r\nADD supervisord-sshd.conf \/etc\/supervisor\/conf.d\/supervisord-sshd.conf\r\n\r\n# Remove pre-installed database\r\nRUN rm -rf \/var\/lib\/mysql\/*\r\n\r\n# Add MySQL utils\r\nADD create_mysql_admin_user.sh \/create_mysql_admin_user.sh\r\nADD import_sql.sh \/import_sql.sh\r\nADD create_db.sh \/create_db.sh\r\nRUN chmod 755 \/*.sh\r\n\r\n# init the databases\r\nRUN mysql_install_db --user=mysql \r\n\r\n# config to enable .htaccess\r\nADD apache_default \/etc\/apache2\/sites-available\/000-default.conf\r\nRUN a2enmod rewrite\r\n\r\n# Configure \/app folder with sample app\r\nRUN git clone https:\/\/github.com\/fermayo\/hello-world-lamp.git \/app\r\nRUN mkdir -p \/app &amp;&amp; rm -fr \/var\/www\/html &amp;&amp; ln -s \/app \/var\/www\/html\r\n\r\nEXPOSE 22 8080 \r\nCMD [\"\/run.sh\"]\r\n<\/pre>\n<p>The other files needed in the docker directory are contained the following tar.gz file: <a href=\"http:\/\/www.opencloudblog.com\/wp-content\/uploads\/2014\/10\/docker-lamp.tar.gz\">Additional LAMP files<\/a><\/p>\n<p>Now it&#8217;s time to build the LAMP Docker container using the command<\/p>\n<pre class=\"lang:sh decode:true\" title=\"Build the LAMP Docker container\">docker build --no-cache --force-rm=true -t wp\/lamp .\r\n<\/pre>\n<h2>Copy WordPress data<\/h2>\n<p>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)<\/p>\n<pre class=\"lang:sh decode:true\" title=\"Copy the WordPress static data\"># assume, that rsync can be used via ssh, copy the wordpress static data\r\n#\r\nrsync -av -e ssh &lt;user&gt;@&lt;yoursite&gt;:&lt;your wordpress directory&gt; .\/data\/\r\n<\/pre>\n<p>copies the static content to the local directory .\/data\/ .<\/p>\n<p>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:<\/p>\n<pre class=\"lang:sh decode:true\" title=\"Copy the mysql database (hosted at Strato )\">#!\/bin\/bash\r\n#\r\nUSERID='&lt;the user id of your database&gt;'\r\nDBNAME='&lt;the name of the database&gt;'\r\nPASSWORD='&lt;the password of your database&gt;'\r\n#\r\nSSH='ssh -n &lt;your login&gt;@ssh.strato.de '\r\nSCP=\"scp -p &lt;your login&gt;@ssh.strato.de\"\r\n#\r\n# get the name of the last backup\r\n#\r\nLASTDB=$($SSH mysqlbackups ${USERID} | head -1)\r\n#\r\nDATUM=$(date +%s)\r\n#\r\nDUMPFILE=\"wp-database-backup.sql\"\r\n#\r\n# make a copy of the database backup\r\n#\r\n$SSH \"mysqldump --add-drop-table -h ${LASTDB} -u ${USERID} -p${PASSWORD} ${DBNAME} &gt; ${DUMPFILE}\"\r\nXDIR=.\r\nX=$(${SCP}:${XDIR}\/${DUMPFILE} .)\r\n#<\/pre>\n<h2>Create the directory to for the WordPress container<\/h2>\n<p>Now you must create the directory for the WordPress docker container data. This directory contains the docker manifest and the static data.<\/p>\n<h2>Post process the WordPress data<\/h2>\n<p>After you have downloaded the WordPress data from your WordPress site, it is necessary to postprocess the data.<\/p>\n<ul>\n<li>Create a tar gz file of the static content<\/li>\n<li>Change the WordPress &#8222;site url&#8220; in the mysql backup to 127.0.0.1:8080<\/li>\n<li>Get the wp-config.php file from the static data and change the parameters for the mysql database<\/li>\n<\/ul>\n<p>The compression of the static data is done by the following script:<\/p>\n<pre class=\"lang:sh decode:true\" title=\"Compress the static WordPress data\">#!\/bin\/bash\r\n#\r\ncd &lt;path to static data&gt;\r\n# fix the permissions\r\nfind . -type d -exec chmod 0755 {} \\;\r\nfind . -type f -exec chmod 0644 {} \\;\r\n# create a tar.gz file in the WP container directory\r\ntar -clSzpf wordpress.tar.gz .\/\r\n<\/pre>\n<p>Changing the WordPress site URL is done using the following script:<\/p>\n<pre class=\"lang:sh decode:true\" title=\"Change the site URL\">#!\/bin\/bash\r\ncd &lt;path to static data&gt;\r\n# change the sed command and replace www.opencloudblog.com by the url of your site \r\ncat wp-database-backup.sql | sed 's#www\\.opencloudblog\\.com#127.0.0.1:8080#g' &gt; wordpress.sql \r\npython &lt;wp docker directory&gt;\/sfix.py wordpress.sql<\/pre>\n<p>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 <a title=\"PHP serialization fix\" href=\"https:\/\/gist.github.com\/astockwell\/6489489\">https:\/\/gist.github.com\/astockwell\/6489489<\/a>:<\/p>\n<pre class=\"lang:python decode:true\" title=\"Python helper script\">#!\/usr\/bin\/env python\r\n#\r\n# Source: https:\/\/gist.github.com\/astockwell\/6489489\r\n\r\nimport os, re\r\n\r\n# Regexp to match a PHP serialized string's signature\r\nserialized_token = re.compile(r\"s:(\\d+)(:\\\\?\\\")(.*?)(\\\\?\\\";)\")\r\n\r\n# Raw PHP escape sequences\r\nescape_sequences = (r'\\n', r'\\r', r'\\t', r'\\v', r'\\\"', r'\\.')\r\n\r\n# Return the serialized string with the corrected string length\r\ndef _fix_serialization_instance(matches):\r\n  target_str = matches.group(3)\r\n  ts_len = len(target_str)\r\n  \r\n  # PHP Serialization counts escape sequences as 1 character, so subtract 1 for each escape sequence found\r\n  esc_seq_count = 0\r\n  for sequence in escape_sequences:\r\n      esc_seq_count += target_str.count(sequence)\r\n  ts_len -= esc_seq_count\r\n  \r\n  output = 's:{0}{1}{2}{3}'.format(ts_len, matches.group(2), target_str, matches.group(4))\r\n  return output\r\n    \r\n# Accepts a file or a string\r\n# Iterate over a file in memory-safe way to correct all instances of serialized strings (dumb replacement)\r\ndef fix_serialization(file):\r\n  try:\r\n    with open(file,'r') as s:\r\n      d = open(file + \"~\",'w')\r\n      \r\n      for line in s:\r\n        line = re.sub(serialized_token, _fix_serialization_instance, line)\r\n        d.write(line)\r\n        \r\n      d.close()\r\n      s.close()\r\n      os.remove(file)\r\n      os.rename(file+'~',file)\r\n      print \"file serialized\"\r\n      return True\r\n  except:\r\n    # Force python to see escape sequences as part of a raw string (NOTE: Python V3 uses `unicode-escape` instead)\r\n    raw_file = file.encode('string-escape')\r\n    \r\n    # Simple input test to see if the user is trying to pass a string directly\r\n    if isinstance(file,str) and re.search(serialized_token, raw_file):\r\n      output = re.sub(serialized_token, _fix_serialization_instance, raw_file)\r\n      print output\r\n      print \"string serialized\"\r\n      return output\r\n    else:\r\n      print \"Error Occurred: Not a valid input?\"\r\n      exit()\r\n\r\nif __name__ == \"__main__\":\r\n  import sys\r\n  \r\n  try:\r\n    fix_serialization(sys.argv[1])\r\n  except:\r\n    print \"No File specified, use `python serialize_fix.py [filename]`\"\r\n\r\n<\/pre>\n<p>Place sfix.py in the wp container directory.<\/p>\n<p>Do not forget to save a copy of the wp-config.php file to the wp container directory and set the following values to:<\/p>\n<pre class=\"lang:php decode:true\" title=\"wp-config.php config changes for database in the docker container\">\/\/ ** MySQL settings - You can get this info from your web host ** \/\/\r\n\/** The name of the database for WordPress *\/\r\ndefine('DB_NAME', 'wordpress');\r\n\/** MySQL database username *\/\r\ndefine('DB_USER', 'root');\r\n\/** MySQL database password *\/\r\ndefine('DB_PASSWORD', '');\r\n\/** MySQL hostname *\/\r\ndefine('DB_HOST', 'localhost');\r\n<\/pre>\n<h2>The WordPress Docker Container<\/h2>\n<p>The wordpress docker container is build using the following Dockerfile:<\/p>\n<pre class=\"lang:sh decode:true \" title=\"Wordpress Dockerfile\">#\r\n# docker build --no-cache --force-rm=true -t wp\/blog .\r\n#\r\n# docker run -d -p 8080:8080 -p 8022:22 --name=oc wp\/blog\r\n#\r\nFROM wp\/lamp:latest\r\nMAINTAINER RTR\r\n\r\n# Configure WordPress to connect to local DB\r\nADD wp-config.php \/app\/wp-config.php\r\n\r\n# save the original config\r\nRUN mv -v \/app\/wp-config.php \/app\/wp-config.php.org\r\n\r\n# copy the wordpress stuff from strato\r\nADD wordpress.tar.gz \/app\/\r\n\r\n# save the strato config\r\nRUN mv -v \/app\/wp-config.php \/app\/wp-config.php.strato\r\n\r\n# restore the original config\r\nRUN cp -v \/app\/wp-config.php.org \/app\/wp-config.php\r\n\r\n# add the owner of the content and change the owner and Modify permissions to allow plugin upload\r\nRUN useradd -d \/var\/www -g www-data -M -s \/usr\/sbin\/nologin -c \"Owner of the content\" www-owner &amp;&amp;\\\r\n    chown -R www-data:www-data \/app &amp;&amp;\\\r\n    find \/app -type d -exec chmod 775 {} \\; \r\n\r\n# Modify permissions to allow plugin upload\r\n# RUN chmod -R 777 \/app\/wp-content\r\n\r\n# copy the database backup\r\nADD wordpress.sql \/root\/\r\n\r\n# copy the db import script\r\nADD import-db.sh \/root\/\r\n  \r\nRUN  bash \/root\/import-db.sh\r\n\r\n# apache listens to 8080\r\nRUN echo \"Listen 8080\" &gt; \/etc\/apache2\/ports.conf\r\n\r\nEXPOSE 22 8080\r\nCMD [\"\/run.sh\"]\r\n<\/pre>\n<p>The Dockerfile requires a helper script to import the database:<\/p>\n<pre class=\"lang:sh decode:true \" title=\"Create and import the database\">#!\/bin\/bash\r\n\r\n\/usr\/bin\/mysqld_safe &gt; \/dev\/null 2&gt;&amp;1 &amp;\r\n\r\nRET=1\r\nwhile [[ RET -ne 0 ]]; do\r\n    echo \"=&gt; Waiting for confirmation of MySQL service startup\"\r\n    sleep 5\r\n    mysql -uroot -e \"status\" &gt; \/dev\/null 2&gt;&amp;1\r\n    RET=$?\r\ndone\r\n\r\nmysql -u root -e \"create database wordpress;\"\r\nmysql -u root wordpress &lt; \/root\/wordpress.sql\r\nls -la \/var\/lib\/mysql\/\r\n\r\nmysqladmin -uroot shutdown\r\n<\/pre>\n<p>To build the image, you need to run<\/p>\n<pre class=\"lang:sh decode:true \" title=\"Build the docker container\">docker build --no-cache --force-rm=true -t wp\/blog .\r\n<\/pre>\n<p>Check with the command docker images, that the image has been created:<\/p>\n<pre class=\"lang:sh decode:true \" title=\"docker images\"># docker images\r\nREPOSITORY             TAG                 IMAGE ID            CREATED             VIRTUAL SIZE\r\nwp\/blog                latest              4fffcac92b7d        9 seconds ago       677 MB\r\nubuntu                 14.04               accae238329d        3 weeks ago         221.1 MB\r\nwp\/lamp                latest              932e7a77f9dd        2 hours ago         518.2 MB\r\n<\/pre>\n<h2 class=\"lang:sh decode:true  \" title=\"docker images\">Run the container<\/h2>\n<p class=\"lang:sh decode:true  \" title=\"docker images\">Now it&#8217;s time to run your copy of your wordpress site. Create a container using the command:<\/p>\n<pre class=\"lang:sh decode:true\" title=\"Create a container\">docker run -d -p 8080:8080 -p 8022:22 --name=wordpresscopy wp\/blog\r\n# check if the container is running\r\ndocker ps\r\n<\/pre>\n<p>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,&#8230; in the container.\u00a0 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.<\/p>\n<p>You can login to the container using ssh to port 8022 (ssh -l root -p 8022 127.0.0.1)<\/p>\n<pre class=\"lang:sh decode:true\" title=\"docker status\">CONTAINER ID   IMAGE            COMMAND    CREATED         STATUS          PORTS                                        NAMES\r\n81595c6da3c2   wp\/blog:latest   \"\/run.sh\"  29 seconds ago  Up 28 seconds   0.0.0.0:8022-&gt;22\/tcp, 0.0.0.0:8080-&gt;8080\/tcp wordpresscopy<\/pre>\n<p>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:<\/p>\n<pre class=\"lang:sh decode:true\" title=\"From a container to an image\">#\r\n# stop the conatiner\r\ndocker stop wordpresscopy\r\n#\r\n# get the ID of the created docker container\r\nID=$(docker inspect --format=\"{{ .Id }}\" wordpresscopy)\r\n#\r\n# create an image from a container\r\ndocker commit ${ID} wordpress\/archive-2014-11\r\n#\r\n# list all images\r\ndocker images\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>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&#8217;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 [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12,13],"tags":[],"_links":{"self":[{"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=\/wp\/v2\/posts\/511"}],"collection":[{"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=511"}],"version-history":[{"count":32,"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=\/wp\/v2\/posts\/511\/revisions"}],"predecessor-version":[{"id":914,"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=\/wp\/v2\/posts\/511\/revisions\/914"}],"wp:attachment":[{"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=511"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=511"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.opencloudblog.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=511"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}