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.
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 containercontainer_name
specifies a name for the containerrestart
defines the container restart policyenv_file
tells Compose that we would like to add environment variables from a file called.env
, located in our build contextenvironment
allows us to add additional environment variables, beyond those defined in our.env
filevolumes
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.d
directory 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
- local
networks
specifies the network that will be shared by our applicationsdepends_on
ensures that our containers will start in order of dependency, with thewp_myblog
container starting after themysql_myblog
containerports
option exposes port1234
, we want WordPress to be accessible via the port 1234 in our hostcommand
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-renewal
to 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 port80
, respectively443
server_name
defines our server name and the server block that should be used for requests to our serverrewrite
is being used to redirect all HTTP traffic to HTTPSindex
defines the files that will be used as indexes when processing requests to your serverroot
names the root directory for requests to our serverlocation /
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 😍
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
- Docker Engine overview | Docker Documentation
- Environment variables in Compose | Docker Documentation
- Get Docker Engine – Community for Ubuntu | Docker Documentation
- GitHub – jwilder/nginx-proxy: Automated nginx proxy for Docker containers using docker-gen
- GitHub – selloween/docker-multi-WordPress: Run multiple WordPress Docker containers with NGINX Proxy, LetsEncrypt and PHP Composer
- Host multiple websites on one VPS with Docker and Nginx
- Hosting WordPress over HTTPS with Docker – DEV Community 👩💻👨💻
- How To Install WordPress With Docker Compose | DigitalOcean
- How to restore a MySQL database using Docker
- Learn DevOps basics with this free 2-hour Docker course
- Local WordPress Development with Docker
- Move existing WordPress site into Docker – Dots and Brackets: Code Blog
- Moving a WordPress site into a Docker Container | Stephen AfamO’s Blog
- Multiple WordPress Sites on Docker | Autoize
- MySQL :: MySQL Installation Guide :: 7.3.1 Basic Steps for MySQL Server Deployment with Docker
- Play with Docker
- Putting Multiple WordPress Containers into Production – Proxy Container – PattonWebz
- Vagrant vs. Docker: Which Is Better for Software Development Environments? – DZone DevOps