Docker Lab – Containerising your website, Part 4 (Let’s Encrypt)

If you’ve been following along with this series of tutorials, you’ll have built a LEMP stack capable of handling multiple vhosts. That’s all very well – but if you expect your users to enter their username and password into the site then you’ll need to provide them with a little security. The only way to do that is with ssl, certificates for which used to be pricey or came with strings attached.

Let’s Encrypt is a certificate authority started by the Electronic Frontier Foundation, Mozilla Foundation, University of Michigan, Akamai Technologies and Cisco Systems to provide ssl certificates for free, and to simplify the process of securing your website into the bargain. They take donations though – and this is one project that you really should consider supporting.

I’ve used Let’s Encrypt on other websites and the process has always been very simple – so I naively assumed that securing my Dockerised website would be nice and simple too. It wasn’t. It’s not that the process is particularly difficult or time consuming – it’s just that it took a little while to work out the incantations.

These instructions assume that you’ve followed the steps in the other tutorials of this series.

Configuring your Maintenance Container

It may be possible to generate your certificates in your main website container but if it is then I couldn’t work out how to do it easily. I created a new container just for maintenance.

cd ~ 
mkdir -p maint-compose/public 
mkdir -p lemp-compose/certificates

Now copy your existing certificates out of the nginx container. This step may not be necessary, but since the nginx configuration references these dummy certificates it’s do this step or remove the references to the dummies in the nginx.conf. I leave it up to you to decide which you prefer.

cd ~/lemp-compose/certificates
docker cp lempcompose_nginx_1:/opt/bitnami/nginx/conf/bitnami/certs/server.crt .
docker cp lempcompose_nginx_1:/opt/bitnami/nginx/conf/bitnami/certs/server.key .

Setting up the Maintenance Container

Next create your configuration file – docker-compose.yml.  You might use vi or emacs to edit the file, but I prefer the easy life so I’ll be using nano.  The important thing is what the yml file contains:

cd ~/maint-compose 
nano docker-compose.yml

Add the following lines to the docker-compose.yml file

version: '2'

services:
 nginx:
 image: 'bitnami/nginx:latest'
 ports:
 - '8080:8080'
 - '80:80'
 volumes:
 - ./public:/app
 - ../lemp-compose/certificates:/bitcerts

Starting the Maintenance Container

Now your website is going to need a little downtime. Fortunately this doesn’t take long, and the certificates only need to be renewed once every six months – so this outage shouldn’t have too much impact on your site availability. If your site is critical then consider using a load balancer like Amazon’s excellent Elastic Load Balancer so that you can failover to the ‘B’ side during certificate updates.

cd ~/lemp-compose
docker-compose stop
cd ~/maint-compose
docker-compose up -d

Generating Certificates

Now your website is down you’ll want to carry out these next steps as quickly as possible in order to ensure that there’s minimal outage. In fact, it should be fairly simple to refactor these steps and script them so that the outage is limited to a few seconds – but I’ll leave that as an exercise for you.

Log in to the maintenance container as follows:

docker exec -i -t --user root maintcompose_nginx_1 bash

Now execute the following commands in the container:

install_packages wget xz-utils
cd /tmp
curl -s https://api.github.com/repos/xenolf/lego/releases/latest | grep browser_download_url | grep linux_amd64 | cut -d '"' -f 4 | wget -i -
tar xf lego_linux_amd64.tar.xz
mv lego_linux_amd64 /usr/local/bin/lego

Generate the certificate for your website, noting that you can put in as many domain parameters as you have domains to register.

lego --email="<website owner email address>" --domains="website domain name" --path="/etc/lego" run

Provided that no errors have been raised your certificates will now have been generated. Copy them out of the container.

cp /etc/lego/certificates/*.key /bitcerts
cp /etc/lego/certificates/*.crt /bitcerts

Now exit out of your maintenance container and shut it down.

exit
docker-compose stop

Configuration Updates

If this is the first time you’ve enabled ssl on your lemp stack you’ll need to make a few minor changes.

First, edit your docker-compose.yml file so that you can access your new certificates. Add this volume line to the nginx section:

- ./certificates:/bitnami/nginx/conf/bitnami/certs

Your docker-compose.yml file should now look something like this:

version: '2'

networks:
 app-tier:
  driver: bridge

services:
 maria-db:
  image: 'bitnami/mariadb:latest'
  networks:
   - app-tier
  environment:
   - MARIADB_ROOT_PASSWORD=<Your Root Password Goes Here>
  volumes:
   - ./db-data:/bitnami
 nginx:
  image: 'bitnami/nginx:latest'
  depends_on:
   - phpfpm-<your website name>
  networks:
   - app-tier
  ports:
   - '80:8080'
   - '8080:8080'
   - '443:8443'
   - '8443:8443'
  volumes:
   - ./nginx/nginx.conf:/bitnami/nginx/conf/nginx.conf
   - ./nginx/fastcgi.conf:/bitnami/nginx/conf/fastcgi.conf
   - ./logs:/opt/bitnami/nginx/logs
   - ./nginx/vhosts:/bitnami/nginx/conf/vhosts
   - ./public:/opt/bitnami/nginx/html
   - ./certificates:/bitnami/nginx/conf/bitnami/certs
 phpfpm-<your website name>:
  image: 'bitnami/php-fpm:latest'
  networks:
   - app-tier
  volumes:
   - ./public/<your website name>/htdocs:/app
   - ./logs/<your website name>-php:/opt/bitnami/php/log

Now you’ll need to make a change to the vhost configuration for your website. The changes we want to make will make nginx load the certificates and also force traffic over ssh (so that our users don’t accidentally connect unencrypted, which might introduce the risk that they enter secret information without having a secured connection).

nano ~/nginx/vhosts/sites-available/<your website name>.conf

Now edit your configuration to add your certificates – you do this by adding the ssl_certificate and ssl_certificate_key lines in your server block:

ssl_certificate /bitnami/nginx/conf/bitnami/certs/www.<your website name>.com.crt;
ssl_certificate_key /bitnami/nginx/conf/bitnami/certs/www.<your website name>.com.key;

Finally, lets force the connection onto https (i.e. let’s force it to be secure). We can do this by removing the listen directive for port 8080 (i.e. unsecured connections) so that our website only responds to ssl secured connections. We still need to respond to port 8080 though in order to ensure that the website loads on unsecured connections by automatically switching to a secure connection. Do this by adding a new server block:

server {
 listen 0.0.0.0:8080;
 server_name www.<your website name>.com;
 return 301 https://$server_name$request_uri;
}

After all that your configuration should look something like this:

server {
 listen 0.0.0.0:8080;
 server_name www.<your website name>.com;
 return 301 https://$server_name$request_uri;
}

server {
 listen 0.0.0.0:8443;
 server_name www.<your website name>.com;
 ssl_certificate /bitnami/nginx/conf/bitnami/certs/www.<your website name>.com.crt;
 ssl_certificate_key /bitnami/nginx/conf/bitnami/certs/www.<your website name>.com.key;
 server_tokens off;
 
 error_log "/opt/bitnami/nginx/logs/<your website name>-error.log";
 access_log "/opt/bitnami/nginx/logs/<your website name>-access.log";

location / {
 try_files $uri $uri/index.php;
 }

location ~ \.php$ {
 # fastcgi_pass [PHP_FPM_LINK_NAME]:9000;
 fastcgi_pass phpfpm-<your website name>:9000;
 fastcgi_index index.php;
 include fastcgi.conf;
 root /app;
 fastcgi_intercept_errors on;
 }

location ~ \.htm$ {
 root /opt/bitnami/nginx/html/<your website name>/htdocs;
 }
 
 location ~* \.(js|css|png|jpg|jpeg|gif|ico|dmg|zip)$ {
 expires max;
 log_not_found off;
 root /opt/bitnami/nginx/html/<your website name>/htdocs;
 }
}

Final touches

Even though those last few steps won’t be necessary for renewal, the next step definitely is. Right now, nginx won’t be able to load your certificate – it doesn’t have permission. Make good this problem as follows:

cd ~/lemp-compose/certificates
chmod 664 *

Restart your Containers

If you’ve carried out all of those steps successfully then you can restart your containers and enjoy your newly secured website – thanks to Let’s Encrypt!

cd ~/lemp-compose
docker-compose up -d

This series wouldn’t have been possible without Bitnami and Let’s Encrypt, let alone nginx, MariaDB and PHP-FPM. I’m haven’t been sponsored by them to write these articles – I’ve just found them to be genuinely the best solution for hosting my websites.

Leave a Reply

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