What are we doing here?
Let's set up HAProxy with some lovely free certs from Let's Encrypt via certbot for a couple of domains. Everything running in docker, and all tied together with docker-compose. We'll use docker user-defined networks, because that's the Right Thing To Do.
This should be easy. Right?
Docker: easy. HAProxy: easy. Let's Encrypt: easy. Docker and HAProxy and Let's Encrypt: minor pain in the arse.
There's a few things that make this a bit of a hassle:
- We want haproxy to be running on port 80/443, but those are the ports certbot needs to do validation
We'll have to do this in two stages. - haproxy with the default config won't start up if it can't resolve the container IPs for the backends.
Since certbot is just a command to be run in a container, it probably won't be running when haproxy starts up.
Some extra config is needed in haproxy. - certbot needs to be run once in one way to request the certs, and then every couple of days/weeks in another way to check and renew certs.
We'll need two different incantations for certbot. - When the certs are renewed, we'll need to tell haproxy to pick them up
Some docker-in-docker magic is required. - certbot doesn't know how to make haproxy-complicit cert pem files
We'll need to do a little scripting.
Stage 0 - setup
This all assumes that your soon-to-be certificated domains' A records are all pointing at the docker host (or port-forwarding router, or whatever), and you can reach the docker host on port 80 and 443 from the Interwebs. Your docker host should have docker and docker-compose installed, and docker running.
Stage 1 - get some certs
Since this is a greenfield setup, we can let certbot take care of the initial cert request on its own - HAProxy should be down for this. The Dockerfile for the letsencrypt image looks like:
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository ppa:certbot/certbot && \
apt-get update && \
apt-get install -y certbot docker.io
COPY deploy-hook /deploy-hook
RUN chmod +x /deploy-hook
Note we're installing the docker.io package, and copying in a script. We'll need them later on.
The deploy-hook script looks like:
#!/usr/bin/env bash
cat /etc/letsencrypt/live/wiki.davidstark.name/fullchain.pem \
/etc/letsencrypt/live/wiki.davidstark.name/privkey.pem \
> /etc/letsencrypt/haproxy.pem \
&& docker kill -s HUP haproxy
To run the container, we'll wrap it up in a docker-compose file. We'll call it docker-compose-stage1.yml.
version: '3'
letsencrypt:
build: .
image: letsencrypt
container_name: letsencrypt
restart: no
volumes:
- letsencrypt_etc:/etc/letsencrypt
command: bash -c 'certbot certonly \
--standalone \
--preferred-challenges http-01 \
--http-01-port 8000 \
--agree-tos \
--non-interactive \
-m your.email@fastmail.com \
-d "domain1.example.com" \
-d "domain2.example.com"; \
cat /etc/letsencrypt/live/domain1.example.com/fullchain.pem \
/etc/letsencrypt/live/domain1.example.com/privkey.pem \
> /etc/letsencrypt/haproxy.pem'
ports:
- 80:8000
volumes:
letsencrypt_etc:
In the above we're requesting certs for domain1 and domain2 under example.com. Replace the email too if you're playing along at home. certbot listens on port 8000, which docker is mapping to port 80 and making available to the outside world for Let's Encrypt to talk to. We don't need port 443 mapped, because this is an initial request, so Let's Encrypt should be fine with port 80. Also of note, we're attaching a Volume to /etc/letsencrypt - that's where the certs end up, and that's how we'll make them available to haproxy. The command concatenates the cert chain and private key into a format that haproxy understands, and dumps it out into the mounted /etc/letsencrypt volume.
With all three files in your current dir, run: docker-compose -i docker-compose-stage1.yml up and you should hopefully see a message like:
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at: