Docker - A Cool Option#
I decided to upgrade the VPS from Ubuntu 14.04 to 16.04. At the same time, I decided to use Docker containers to compartmentalize the blog. Docker is very cool technology that does for applications what version control has done for code. It allows you to create a container that has all of the dependencies of an application in one immutable container. This means, for me at least, that it is straightforward to upgrade the different pieces.
I have an nginx container that links to a ghost container. All of the data and configuration files are stored on local volumes which are easily accessible for backup and configuration tunning.
So far it is quite nice! I imagine the real power comes when I try to have multiple web apps running alongside the blog. It will be a simple matter of linking the nginx container to the new apps so it can adequately proxy them. It should be quite an exciting and robust system.
Setup#
I use CloudA as a hosting provider. I have used them for the past few years and haven’t had any problems that I haven’t caused myself. I was using a base image of Ubuntu 14.04. I was also investigating docker technology on my local system. After successfully setting up Syncthing in a Docker container I thought it would be a good idea to containerize my website.
The process I decided was to shut down the current image and spin up a brand new image of Ubuntu 16.04. This process was relatively easy and painless. Once I had a proper ssh session, I was able to use a proper terminal instead of the web-based console that was provided.
Installing Docker#
Make sure the repository is up to date and that everything is upgraded to the latest version:
$ sudo apt-get update
$ sudo apt-get -y upgrade
Install packages to allow apt to use a repository over HTTPS:
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
Add Docker’s official GPG key:
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
Verify that the key fingerprint is 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88.
$ sudo apt-key fingerprint 0EBFCD88
Add the repository:
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
Update the apt package index:
$ sudo apt-get update
Install the latest version of Docker CE. Any existing installation of Docker will be replaced:
$ sudo apt-get install docker-ce
Create the Docker Group#
Create the docker group:
$ sudo groupadd docker
Add your user to the docker group:
$ sudo usermod -aG docker $USER
Note
$USER is the username that you want to add to the docker group
Verify that you can run docker commands without sudo:
$ docker run hello-world
Creating the Required Images#
Create the required folders:
$ mkdir ~/docker
$ mkdir ~/docker/ghost
$ mkdir ~/docker/nginx
$ mkdir -p ~/docker/nginx/{logs,ssl,sites-enabled,static}
Ghost storage container:
$ docker create \
-v /home/troy/docker/ghost/:/var/lib/ghost/content \
--name=ghost-storage alpine
Ghost app:
$ docker create \
--name=ghost \
--restart always \
--volumes-from=ghost-storage \
-e NODE_ENV=production \
-e url=https://bluebill.net \
ghost:alpine
Note
If you don’t set the environment variables in the container Ghost will run in development mode. Also note, you need the proper redirect settings for the nginx configuration otherwise you will encounter some weird redirect errors.
Create the nginx volumes container:
$ docker create \
-v ~/docker/nginx/logs:/var/log/nginx \
-v ~/docker/nginx/ssl:/etc/ssl \
-v ~/docker/nginx/sites-enabled:/etc/nginx/conf.d \
-v ~/docker/nginx/static:/www/data \
--name=nginx-storage alpine
Create the nginx container:
$ docker create \
--name=nginx \
--restart always \
--volumes-from=nginx-storage \
-p 80:80 \
-p 443:443 \
--link ghost:ghost \
nginx:alpine
Note
For testing, it might be prudent to leave the –restart flag out for the time being.
Let’s Encrypt#
I will be using a script called dehydrated to handle the certificate process.
Create the location to store the dehydrated script and configuration files:
$ mkdir ~/docker/dehydrated
$ cd ~/docker/dehydrated
Create a file to store the script:
$ touch dehydrated
Copy the script from the repo:
$ vi dehydrated
Create the config file:
$ touch domains.txt
Copy the contents of the example config file to domains.txt, altering the following:
WELLKNOWN="/home/troy/docker/nginx/static"
Create the domains.txt:
$ touch domains.txt
$ vi domains.txt
Add the following line to the domains.txt:
bluebill.net www.bluebill.net
Launch the script:
$ ./dehydrated -c
The certificates are here:
~/docker/dehydrated/certs/bluebill.net/fullchain.pem
~/docker/dehydrated/certs/bluebill.net/privkey.pem
Copy the files to:
~/docker/nginx/ssl
Note
The reason to copy the files is because symbolic links are not followed if they are outside the docker volumes.
Renewal#
You can add a crontab entry to run the dehydrated script automatically:
$ crontab -e
@weekly dehydrated -c
You will also need an entry to a script that will copy the certificate and private key to the proper spot.
Changes to Nginx to Support Let’s Encrypt and Properly redirecting Ghost in production#
ghost.conf
server {
listen 0.0.0.0:80;
server_name www.bluebill.net bluebill.net;
location '/.well-known/acme-challenge' {
default_type "text/plain";
alias /www/data ;
autoindex on;
}
#make sure we redirect to anything other than the let's encrypt challenge folder.
location / {
return 301 https://$host$request_uri;
}
}
ghost-ssl.conf
server {
listen 0.0.0.0:443 ssl http2;
server_name www.bluebill.net bluebill.net;
ssl_certificate /etc/ssl/fullchain.pem;
ssl_certificate_key /etc/ssl/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
#------------
ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4';
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
#------------
#Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam /etc/ssl/certs/dhparam.pem;
#OCSP Stapling ---
#fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;
#enable session resumption to improve https performance
#http://vincent.bernat.im/en/blog/2011-ssl-session-reuse-rfc5077.html
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
#https://github.com/TryGhost/Ghost/issues/2796
#had to set the proper headers so that ghost will work in production.
location / {
proxy_pass http://ghost:2368;
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 https;
proxy_set_header HOST $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_redirect off;
proxy_intercept_errors on;
}
}
Create the nginx volumes container:
$ docker create \
-v ~/docker/nginx/logs:/var/log/nginx \
-v ~/docker/nginx/ssl:/etc/ssl \
-v ~/docker/nginx/sites-enabled:/etc/nginx/conf.d \
-v ~/docker/nginx/static:/www/data \
--name=nginx-storage alpine
Create the nginx container:
$ docker create \
--name=nginx \
--restart always \
--volumes-from=nginx-storage \
-p 80:80 \
-p 443:443 \
--link ghost:ghost \
nginx:alpine
$ docker start ghost nginx