WordPress Migration Saga – Step By Step Guide for Ubuntu 18.04

8 min read
from Le blog de Laurel

Here is the third post, and last, from WordPress Migration Saga, a series that presents the migration of an existing WordPress website to another host, in Docker.

In this post we will see step by step how the migration of a WordPress website to Docker can be made on Linux.

To get a little aquainted with Docker and Docker Compose working environment, read Getting Used to Docker first.

Setting Up the Website Folder Structure

In order to keep our data in the new Docker container, we will have to store the data in a directory volume which we will mount unto our Docker container.

First, unless we already had the zip, let’s compress our WordPress site in a zipped file named site.zip, on our local computer. You can use the zip utility from inside the folder, to remove the parent folder from the final zip file.

cd local/site1.com; zip -r ../site.zip *

Next, on Linode we’ll create a folder for our site, and copy the data from our computer to the Linode host. Note that 192.0.2.0 is a dummy IP, change it with the IP of your server.

# on the Linode instance, create the folder and its parents, if they don't exist
mkdir -pv /var/www/site1.com/src
mkdir -pv /var/www/site1.com/database/data
mkdir -pv /var/www/site1.com/database/initdb.d
# from the local computer, copy the zip file and database.sql to the Linode instance
scp local/site1.com/database.sql your_username@192.0.2.0:/var/www/site1.com/database/initdb.d
scp local/site1.com/site.zip your_username@192.0.2.0:/var/www/site1.com
# on the Linode instance, decompress the file
cd /var/www/site1.com/
apt-install unzip
unzip site.zip -d src/
rm site.zip

Our folder should have the following structure:

.
 └── site1.com
     ├── .env
     ├── database
     │   ├── data
     │   └── initdb.d
     │       └── database.sql
     ├── docker-compose.yml
     └── src
         ├── …
         ├── wp-admin
         ├── …
         └── xmlrpc.php

Now that we have decided our folder structure, we create a companion .env file that will store our credentials. You can use vim to create the files, or your favourite editor.

I usually write my files in VSCode, hit Ctrl+C, then go to the command line, vim filename, gg+dG to clear all the content, i to set insert mode, Shift+Ctrl+V to paste the content from VSCode, ESC and :wq! to exit and write the changes. Another option would be to set remote access to the server from VSCode.

DB_ROOT_PASSWORD=my_secret_passwd
DB_NAME=my_secret_name
DB_USER=my_secret_user
DB_PASSWORD=my_secret_passwd2

Optionally, because your .env file contains sensitive information, you can include it in your project’s .gitignore and .dockerignore files, which tell Git and Docker what files not to copy to your Git repositories and Docker images, respectively (if we wanted versioning).

.env
.git
docker-compose.yml
.dockerignore

Now, let’s create the docker-compose.yml file.

version: '3.7'

networks:
    wp-back-myblog:

services:
    mysql_myblog:
        container_name: wp_myblog_db
        image: mysql:5.7
        volumes:
            - ./database/data:/var/lib/mysql
            - ./database/initdb.d:/docker-entrypoint-initdb.d
        restart: always
        env_file: .env
        environment:
            MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD
            MYSQL_DATABASE: $DB_NAME
            MYSQL_USER: $DB_USER
            MYSQL_PASSWORD: $DB_PASSWORD
        networks:
            - wp-back-myblog
        command: 
            --default-authentication-plugin=mysql_native_password

    wp_myblog:
        container_name: wp_myblog
        depends_on:
            - mysql_myblog
        image: WordPress
        volumes:
            - ./src:/var/www/html
        ports:
            - 1234:80
        networks:
            - wp-back-myblog
        restart: always
        env_file: .env
        environment:
            WordPress_DB_HOST: mysql_myblog:3306
            WordPress_DB_NAME: $DB_NAME
            WordPress_DB_USER: $DB_USER
            WordPress_DB_PASSWORD: $DB_PASSWORD

Here we define:

  • the Compose file version
  • two services, one for the database, another for the WordPress application
  • image tells Compose what image to pull to create the container
  • container_name specifies a name for the container
  • restart defines the container restart policy
  • env_file tells Compose that we would like to add environment variables from a file called .env, located in our build context
  • environment allows us to add additional environment variables, beyond those defined in our .env file
  • volumes mounting bind mounts:
    • local ./database/data , mounted to the /var/lib/mysql directory on the container, is the standard data directory for MySQL on most distributions
    • local ./database/initdb.d, mounted on the /docker-entrypoint-initdb.ddirectory on the container, will be used to prepopulate our database
    • local ./src , mounted on the /var/www/html directory on the container, will be our website home
  • networks specifies the network that will be shared by our applications
  • depends_on ensures that our containers will start in order of dependency, with the wp_myblog container starting after the mysql_myblog container
  • ports option exposes port 1234, we want WordPress to be accessible via the port 1234 in our host
  • command MySQL 8.0 changed the default authentication plugin, and older client may not be able to connect, --default-authentication-plugin=mysql_native_password must be added (src)

When mysql_myblog container is started for the first time, a new database with the specified name will be created and initialized with the provided configuration variables. Furthermore, it will execute files with extensions .sh, .sql and .sql.gz that are found in /docker-entrypoint-initdb.d. We will populate our mysql service by mounting a SQL dump into this directory, and SQL files will be imported by default to the database specified by the MYSQL_DATABASE variable. (src)

While bind mounts are dependent on the directory structure of the host machine, volumes are completely managed by Docker. (src)

Now, create the containers with docker-compose up and the -d flag, which will run the mysql_myblog and wp_myblog containers in the background, and check the status of the services.

# check status
docker-compose ps
# see logs
docker-compose logs
# follow specific log for testing
docker logs -f a80

If the services state is up, you should be able to access 192.0.2.0:1234 and see your WordPress website. Next, we will use NGINX to map https://site1.com to our application located at 192.0.2.0:1234.

Using NGINX as a Reverse Proxy to our Docker Containers

We want our website to be secure, so we will enable HTTPS both in the wp-config.php file and in the nginx config file, in order to avoid ‘The page isn’t redirecting properly‘ issues. Let’s add the following constants in wp-config.php

$_SERVER['HTTPS'] = 'on';
define('FORCE_SSL_ADMIN',   true);
define('FORCE_SSL_LOGIN',   true);
define('FORCE_SSL_CONTENT', true);

Before you can actually get a Let’s Encrypt certificate you need to install Certbot. Certbot is the official Let’s Encrypt client and also the easiest way to get a certificate. You can run certbot with the --staging option to test your settings, then change this option to --force-renewalto obtain the final certificates. There are rate limits that assures people don’t overuse the service, make sure you use the staging environment while you are testing.

# stop nginx, certbot conflicts with nginx on port 80
sudo service nginx stop
# install certbot
sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot
# create the certificate
certbot certonly --standalone --preferred-challenges http --email admin@site1.com --noninteractive --quiet --agree-tos -d site1.com -d www.site1.com  --expand
# list the certificates
sudo certbot certificates
# test the renewal script
sudo certbot renew --dry run
# start nginx
sudo service nginx start 

Create a nginx configuration file named site1.com.conf in /etc/nginx/conf.d. This file will allow nginx to do the proper redirects from HTTP to HTTPS, and proxy to the WordPress application.

server {
    listen 80;
    listen [::]:80;

    server_name site1.com www.site1.com;
    rewrite ^ https://$server_name$request_uri? permanent;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name site1.com www.site1.com;

    root /var/www/site1.com;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/site1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/site1.com/privkey.pem;

    gzip on;
    gzip_comp_level 3;
    gzip_types text/css image/jpg image/jpeg image/png image/svg;

    location / {
        proxy_pass http://0.0.0.0:1234;
        proxy_set_header Accept-Encoding "";
        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-Proto $scheme;
    }
}

In the file above, we have the following options:

  • listen tells Nginx to listen on port 80, respectively 443
  • server_name defines our server name and the server block that should be used for requests to our server
  • rewrite is being used to redirect all HTTP traffic to HTTPS
  • index defines the files that will be used as indexes when processing requests to your server
  • root names the root directory for requests to our server
  • location / block is where specific directives will proxy requests to the WordPress application

And… Voilà! If we hit nginx reload we should be able to access our website at https://site1.com 🤗

For debugging, we can use netstat -nlp to check the listening ports, docker ps -a and docker logs -f wp_myblog to follow logs, nginx -t for nginx errors. For backup we can use mysqldump and some cron job with scp for the blog (or versioning).

# test connection to the database
docker exec -it wp_myblog_db mysql -u$DB_USER --password=$DB_PASSWORD $DB_NAME
# backup
docker exec wp_blogmap_db /usr/bin/mysqldump -u root --password=$DB_PASSWORD $DB_NAME > backup.sql
# restore
cat backup.sql | docker exec -i wp_blogmap_db /usr/bin/mysql -u$DB_USER --password=$DB_PASSWORD $DB_NAME

In order to migrate multiple WordPress websites to the same host, you need only to add the folder structure for each of them, add nginx config files for each, and change the port from 1234 to other free ports.

There are many other aspects I won’t touch in this post, like increasing the security, running the containers as current user, using nginx proxy and certbot as Docker images, running a crontab job to update the certbot certificates, CI/CD. How to ditch WordPress and make a Node.js application. How to make sure you’ve mounted the volumes correctly and you don’t lose your data.. Ansible who? 😅

Also, there are many things I’m discovering about Docker right now, this is just the tip of the iceberg. Don’t hesitate to contact me if you saw erroneous information, or my server password in plain text. 😅 I just hope this post was helpful to people who are getting started with Docker and want to learn more about it. 🌱🌱🌞

I’m happy I found out about Le blog de Laurel, too, she makes some amazing illustrations. Ok.. everyday is a Holiday 😍

From Le blog de Laurel

Extra Junk

Example folder structure for multiple blogs and nginx installed on host:

.
 ├── site1.com
 │   ├── .env
 │   ├── database
 │   │   ├── data
 │   │   └── initdb.d
 │   │       └── database.sql
 │   ├── docker-compose.yml
 │   └── src
 │       ├── …
 │       ├── wp-admin
 │       ├── …
 │       └── xmlrpc.php
 └── site2.com
     ├── .env
     ├── database
     │   ├── data
     │   └── initdb.d
     │       └── database.sql
     ├── docker-compose.yml
     └── src
         ├── …
         ├── wp-admin
         ├── …
         └── xmlrpc.php

Example folder structure for using jwilder/nginx-proxy (after this )

.
 ├── site1.com
 │   ├── .dockerignore
 │   ├── .env
 │   ├── .gitignore
 │   ├── database
 │   ├── docker-compose.yml
 │   └── src
 ├── site2.com
 │   ├── .dockerignore
 │   ├── .env
 │   ├── .gitignore
 │   ├── database
 │   ├── docker-compose.yml
 │   └── src
 └── nginx
     └── docker-compose.yml
version: '3'

services:
  nginx:
    image: jwilder/nginx-proxy:alpine
    container_name: nginx
    restart: always
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: 'true'
    ports:
      - 80:80
      - 443:443
    volumes:
      - /srv/nginx/data/certs:/etc/nginx/certs:ro
      - /srv/nginx/data/conf.d:/etc/nginx/conf.d
      - /srv/nginx/data/vhost.d:/etc/nginx/vhost.d
      - /srv/nginx/data/html:/usr/share/nginx/html
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - proxy
      
  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: letsencrypt
    volumes:
      - /srv/nginx/data/vhost.d:/etc/nginx/vhost.d
      - /srv/nginx/data/certs:/etc/nginx/certs:rw
      - /srv/nginx/data/html:/usr/share/nginx/html
      - /var/run/docker.sock:/var/run/docker.sock:ro
    depends_on:
      - nginx
    networks:
      - proxy
    
networks:
  proxy:
    driver: bridge
    jwilder/nginx-proxy 
DB_CONTAINER=my_container

DB_ROOT_PASSWORD=my_passwd
DB_NAME=my_user
DB_PASSWORD=my_passwd2

WP_CONTAINER=my_container2

VIRTUAL_HOST=site1.com,www.site1.com

LETSENCRYPT_EMAIL=admin@site1.com
version: '3'

services:

  db:
    container_name: $DB_CONTAINER
    image: mysql:5.7
    restart: always
    env_file: .env
    volumes:
      - ./database/data:/var/lib/mysql
      - ./database/initdb.d:/docker-entrypoint-initdb.d
    environment:
      MYSQL_RANDOM_ROOT_PASSWORD: $DB_ROOT_PASSWORD
      MYSQL_DATABASE: $DB_NAME
      MYSQL_USER: $DB_USER
      MYSQL_PASSWORD: $DB_PASSWORD
    command: 
      --default-authentication-plugin=mysql_native_password

  wp:
    container_name: $WP_CONTAINER
    depends_on:
      - db
    image: WordPress
    restart: always
    ports:
      - "1234:80"
    volumes:
      - ./src:/var/www/html
    env_file: .env
    environment:
      WordPress_DB_HOST: ${DB_CONTAINER}:3306
      WordPress_DB_NAME: $DB_NAME
      WordPress_DB_USER: $DB_USER
      WordPress_DB_PASSWORD: $DB_PASSWORD
      VIRTUAL_HOST: $VIRTUAL_HOST
      LETSENCRYPT_HOST: $VIRTUAL_HOST
      LETSENCRYPT_EMAIL: $LETSENCRYPT_EMAIL
      LETSENCRYPT_TEST: 'true'

networks:
  default:
    external:
      name: nginx_proxy

Example folder structure for single website and nginx-proxy and certbot as Docker images (after this)

└── site1.com
     ├── .dockerignore
     ├── .env
     ├── .gitignore
     ├── database
     │   ├── data
     │   └── initdb.d
     ├── docker-compose.yml
     ├── nginx-conf
     │   ├── nginx.conf
     │   └── options-ssl-nginx.conf
     └── src
         ├── …
         ├── wp-admin
         └── xmlrpc.php
version: '3'

services:
    db:
        image: mysql:8.0
        container_name: db
        restart: unless-stopped
        env_file: .env
        environment:
            - MYSQL_ROOT_PASSWORD=$DB_ROOT_PASSWORD
            - MYSQL_DATABASE=$DB_NAME
            - MYSQL_USER=$DB_USER
            - MYSQL_PASSWORD=$DB_PASSWORD
        volumes: 
            - ./database/data:/var/lib/mysql
            - ./database/initdb.d:/docker-entrypoint-initdb.d
        command: '--default-authentication-plugin=mysql_native_password'
        networks:
            - app-network
    WordPress:
        depends_on: 
            - db
        image: WordPress:5.1.1-fpm-alpine
        container_name: WordPress
        restart: unless-stopped
        env_file: .env
        environment:
            - WordPress_DB_HOST=db:3306
            - WordPress_DB_USER=$DB_USER
            - WordPress_DB_PASSWORD=$DB_PASSWORD
            - WordPress_DB_NAME=$DB_NAME
        volumes:
            - ./src:/var/www/html
        networks:
            - app-network
    webserver:
        depends_on:
            - WordPress
        image: nginx:1.15.12-alpine
        container_name: webserver
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - ./src:/var/www/html
            - ./nginx-conf:/etc/nginx/conf.d
            - certbot-etc:/etc/letsencrypt
        networks:
            - app-network
    certbot:
        depends_on:
            - webserver
        image: certbot/certbot
        container_name: certbot
        volumes:
            - certbot-etc:/etc/letsencrypt
            - ./src:/var/www/html
        command: certonly --webroot --webroot-path=/var/www/html --email admin@site1.com --agree-tos --no-eff-email --force-renewal -d site1.com -d www.site1.com
volumes:
    certbot-etc:
networks:
    app-network:
        driver: bridge 

References

Leave a Reply

Your email address will not be published. Required fields are marked *