My wife wanted a blog and I decided to start blogging as well as a way to document all the projects I work on.
Therefore, I figured that one of the first posts would be how I set up the blog system.
Of course, I could have just set up an account on the WordPress site, but where’s the fun in that? Instead, I wanted to build a reasonably complete Docker setup and have it deployed to a cloud provider.
Docker Installation
To start, I installed the latest (1.5 as of this post) build of Docker on one of my Linux environments. For the most part, I followed the instructions on:
https://docs.docker.com/installation/ubuntulinux/
I wanted to use the Docker-maintained package installation. My only real issue was that I run my own apt-cacher since I have dozens of virtual environments, and I found that the above instructions used an HTTPS update site that conflicted with apt-cacher. Instead of tracking down a better solution, I simply added
Acquire::HTTP::Proxy::get.docker.com "DIRECT";
to my apt configuration file at /etc/apt/apt.conf.d/01proxy
I also installed docker-machine from
https://docs.docker.com/machine/
and just copied the executable into /usr/local/bin
Setting up the Machine
I decided to try out using Digital Ocean as the cloud provider, since they had one of the cheaper setups ($5/month for a 512MB Memory / 20 Gig SSD / 1 TB data).
Once I had set up an account with Digital Ocean, I went to the Apps & API and then clicked “Generate new Token”.
That generated a very long token key that I stored away. In the following code snippets, I’ll make it available as an environmental variable.
export DO_API_TOKEN=xxxxx
Next, I need to figure out which zone to start the machine in. Unfortunately, Digital Ocean’s website shows nice human-readable names, but not the codes needed by docker-machine. Fortunately, they do have a REST API that I could use.
curl -X GET -u "$DO_API_TOKEN:" https://api.digitalocean.com/v2/regions
This returned a JSON structure (formatted and truncated for readability):
{
"regions" : [{
"name" : "New York 1",
"slug" : "nyc1",
"sizes" : [],
"features" : ["virtio", "backups"],
"available" : false
}, {
"name" : "Amsterdam 1",
"slug" : "ams1",
"sizes" : [],
...
The ‘slug’ element is the one needed. So, if we wanted an box in New York, we’d use nyc1. In my case, I wanted San Francisco, so it was sfo1.
By running the docker-machine create command, we’re created a new machine in San Francisco, with only 512 MB of memory. The final parameter is the name for the machine, which I’ve called wordpress. NOTE: You start incurring charges at this point.
docker-machine create --driver digitalocean --digitalocean-access-token $DO_API_TOKEN --digitalocean-region sfo1 --digitalocean-size "512mb" wordpress
This takes a few minutes to provision and set up the machine, and you should see something like:
INFO[0000] Creating SSH key...
INFO[0000] Creating Digital Ocean droplet...
INFO[0003] Waiting for SSH...
INFO[0066] Configuring Machine...
INFO[0108] "wordpress" has been created and is now the active machine.
INFO[0108] To point your Docker client at it, run this in your shell: $(docker-machine env wordpress)
Now, that you have a machine running, which you can verify at any time with:
docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
wordpress * digitalocean Running tcp://104.236.140.57:2376
Or you can use the Digital Ocean dashboard to see that it’s running:
Now, there are many different ways to issue the commands to the docker machine, but my favorite is to just set up the environment so that regular docker commands works with the remote machine. So, enter:
$(docker-machine env wordpress)
This sets a few environmental variables so that all future docker commands will run remotely.
Setting up the Reverse Proxy
One of the things that I wanted to do was run many different websites from the same host. The easiest way to do that is to run a reverse proxy that internally forwards traffic based on the requested Host parameter to the appropriate docker container. I found a great docker container by Jason Wilder that did everything I needed:
https://registry.hub.docker.com/u/jwilder/nginx-proxy/
So, all I needed to do was to run his nginx reverse proxy container as the front-end.
docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock jwilder/nginx-proxy
Now, any new container that was registered with the right environmental variables would be automatically linked into the proxy container.
Setting up MySQL
Alot of the steps and inspiration for these instructions comes from a blog post:
http://antfie.com/super-easy-dockerised-wordpress/
Thanks out to Anthony Fielding for his set of instructions.
Because I want to be able to reproduce these steps for multiple different sites, I’ll store the site name and the MySQL password in environmental variables:
export SITE_NAME=diamondq
export MYSQL_PASSWORD=xxxx
I want to make it real easy to do management of the database environment, so I created a data container to hold the database data.
docker create --name $SITE_NAME-mysql-data -v /var/lib/mysql mysql
Next, I created the MySQL server itself.
NOTE: This is where I initially ran into a small challenge with the memory constrained environment at Digital Ocean. MySQL didn’t want to initialize because there wasn’t enough RAM. Since this was going to be a very low usage environment for some time, I just added 4 Gigs of disk space as swap. Connecting to the box was easy with the docker-machine command:
docker-machine ssh
Now, that I’m on the box, it was a few standard commands to generate the 4 gigabyte swapfile and make it permanent:
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "/swapfile none swap sw 0 0" >> /etc/fstab
DOUBLE NOTE: When running the MySQL container that failed the first time, it left the data container in a bit of a mess, and I had to remove and restart the data container as well as the MySQL container to get it to work.
By providing some environmental variables, the wordpress database was automatically created.
docker run --name $SITE_NAME-mysql --volumes-from $SITE_NAME-mysql-data -e "MYSQL_ROOT_PASSWORD=$MYSQL_PASSWORD" -e "MYSQL_DATABASE=wordpress" -d mysql
We now have a MySQL database that is configured with an empty wordpress database. NOTE: I’m not exposing the MySQL port, so there is no way that anyone can remotely connect to the database.
Setting up WordPress
Now for WordPress. I originally looked around at the current WordPress container, but I wanted better control over the contents so that I could do things like back it up easier (as did Anthony, which is where I ran across his blog). So, I followed the same pattern as with MySQL. I started by creating a data container:
docker create --name $SITE_NAME-wordpress-data -v /var/www/html ubuntu
Next, I needed to download the current WordPress contents into this container. While this seems like a prime candidate for a separate Dockerfile, I didn’t want all this stored in the Docker AUFS, but instead in the Docker volume.
docker run -it --rm --volumes-from $SITE_NAME-wordpress-data ubuntu /bin/bash -c "apt-get install -y wget && cd /var/www/html && wget https://wordpress.org/latest.tar.gz && tar -xzf latest.tar.gz && mv wordpress/* . && rm -rf wordpress latest.tar.gz && chown -R www-data:www-data /var/www/html"
Since there may be multiple WordPress containers running on the same box, each ones needs a unique port number so that the nginx reverse proxy can talk to it. Thus, we’ll put the unique port number into another environmental variable:
export SITE_PORT=8080
export SITE_HOST=blog.diamondq.com
Now, we just start the final WordPress container:
docker run --name $SITE_NAME-wordpress --volumes-from $SITE_NAME-wordpress-data --link $SITE_NAME-mysql:mysql -p 127.0.0.1:$SITE_PORT:80 -e "VIRTUAL_HOST=$SITE_HOST" -e "VIRTUAL_PORT=$SITE_PORT" -d antfie/wordpress
This is where all the magic comes together. It brings in the WordPress data container so that the WordPress content itself is available. It links in the MySQL database, so it can be contacted internally via a virtual host called ‘mysql’. It exposes the Apache web server as port 8080, but only to the local host. This means that other containers can reach it (like nginx), but not external users. The two environment settings, VIRTUAL_HOST and VIRTUAL_PORT, are provided so that the nginx reverse proxy knows that it needs to link this container into the proxy. And finally, we’re using antfie/wordpress, since it’s basically the normal WordPress install with WordPress removed (since we’re bringing it in ourselves via the data container).
Assuming that your DNS is configured to point your website (blog.diamondq.com in my case) to the IP address of the Digital Ocean machine, everything should be ready to configure WordPress.
http://blog.diamondq.com
The only slight thing to know is that in one of the first configuration screens for WordPress, you must configure the database connection. In this case, the user name is root, the password is whatever is in $MYSQL_PASSWORD (although do not type $MYSQL_PASSWORD, since your browser is not aware of this environmental variable), and the host is the string ‘mysql’ (since that was the virtual host name when you linked in MySQL above).
Recap
For those who just want to do this quickly, it’s just three ‘blocks’:
Setup up the nginx reverse proxy
docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock jwilder/nginx-proxy
Define the environment you want to build
export SITE_NAME=diamondq
export MYSQL_PASSWORD=xxxx
export SITE_PORT=8080
export SITE_HOST=blog.diamondq.com
Build the environment
docker create --name $SITE_NAME-mysql-data -v /var/lib/mysql mysql
docker run --name $SITE_NAME-mysql --volumes-from $SITE_NAME-mysql-data -e "MYSQL_ROOT_PASSWORD=$MYSQL_PASSWORD" -e "MYSQL_DATABASE=wordpress" -d mysql
docker create --name $SITE_NAME-wordpress-data -v /var/www/html ubuntu
docker run -it --rm --volumes-from $SITE_NAME-wordpress-data ubuntu /bin/bash -c "apt-get install -y wget && cd /var/www/html && wget https://wordpress.org/latest.tar.gz && tar -xzf latest.tar.gz && mv wordpress/* . && rm -rf wordpress latest.tar.gz && chown -R www-data:www-data /var/www/html"
docker run --name $SITE_NAME-wordpress --volumes-from $SITE_NAME-wordpress-data --link $SITE_NAME-mysql:mysql -p 127.0.0.1:$SITE_PORT:80 -e "VIRTUAL_HOST=$SITE_HOST" -e "VIRTUAL_PORT=$SITE_PORT" -d antfie/wordpress
If you want a second site on the same box, then you just change the environment and re-run the build instructions. Takes about 30 seconds.