I have a separate administrative server that’s responsible for maintaining certificates via the Matrix organization‘s Docker-Dehydrated image, and then distributing those certificates to the appropriate servers. This article covers the view from my administrative server, but if you’re running a single server, then you can place it alongside Traefik, Nginx, et al. and forgo the distribution section at the end of this article.
Prerequisites
- Docker
- Docker Compose
- systemd template for Docker Compose
- Parallel-Rsync (or you could use Ansible)
Setup
Docker Compose
The highlighted lines need to be changed to suit your needs.
Let’s Encrypt has rate limits, which is why you’ll want to test your configuration before you switch over to production. When you’re certain that everything is working, swap out dehydrated-testing.env
file with dehydrated-production.env
/etc/docker/compose/dehydrated/docker-compose.yaml
version: '2'
services:
dehydrated:
container_name: dehydrated
image: docker.io/matrixdotorg/dehydrated
restart: unless-stopped
volumes:
- /srv/dehydrated/accounts/:/data/accounts/
- /srv/dehydrated/certs/:/data/certs/
- /srv/dehydrated/domains.txt:/data/domains.txt
env_file:
- dehydrated.env
- dehydrated-testing.env
# - dehydrated-production.env
/etc/docker/compose/dehydrated/dehydrated.env
DEHYDRATED_GENERATE_CONFIG=yes
DEHYDRATED_CHALLENGE=dns-01
DEHYDRATED_KEYSIZE=4096
DEHYDRATED_HOOK=/usr/local/bin/lexicon-hook
DEHYDRATED_RENEW_DAYS=30
DEHYDRATED_KEY_RENEW=yes
DEHYDRATED_EMAIL=YOU@EXAMPLE.COM
DEHYDRATED_ACCEPT_TERMS=no
PROVIDER=digitalocean
LEXICON_DIGITALOCEAN_TOKEN=0123456789
Docker-Dehydrated uses Dehydrated for Let’s Encrypt and Lexicon for DNS operations supported by many providers.
If you’re using a provider other than Digital Ocean, you’ll need to swap out LEXICON_DIGITALOCEAN_TOKEN
with the appropriate environment variables. For example, if you were to set PROVIDER
to cloudflare
, you would use LEXICON_CLOUDFLARE_USERNAME
and LEXICON_CLOUDFLARE_TOKEN
.
Double-click to select all, copy, and paste to your own files:
/etc/docker/compose/dehydrated/dehydrated-testing.env
DEHYDRATED_CA="https://acme-staging-v02.api.letsencrypt.org/directory"
/etc/docker/compose/dehydrated/dehydrated-production.env
DEHYDRATED_CA="https://acme-v02.api.letsencrypt.org/directory"
Dehydrated
Now that the Docker Compose side has been configured, set up your data directories and domain configuration:
mkdir -pv /srv/dehydrated/{accounts,certs} \
&& \
touch /srv/dehydrated/domains.txt
Populate /srv/dehydrated/domains.txt
with the domains that you want to use, with one domain on each line, and spaces that separate SANs (Subject Alternative Name) or a wildcard domain (which is only available via DNS-based confirmation).
To set up certificates for Thad.Getterman.org and LTG.FYI, I have the following:
thad.getterman.org www.thad.getterman.org
ltg.fyi *.ltg.fyi
In this example, I’ve created a single SAN so that if someone places www
in front of my name, it will still work. While as with my URL-shortening domain, I’ve created a wildcard domain, so that I can direct traffic based on not only the path but also any sub-domain, all while providing encryption for any combination that’s queried from Traefik.
Now you can see if it works with dehydrated-testing.env
enabled:
cd /etc/docker/compose/dehydrated
docker-compose up && docker-compose down
After several minutes, and if everything worked correctly, you should see success messages about certificates being established for your domain(s):
dehydrated | RESULT
dehydrated | ------
dehydrated | True
dehydrated | + Requesting certificate...
dehydrated | + Checking certificate...
dehydrated | + Done!
dehydrated | + Creating fullchain.pem...
dehydrated | deploy_cert called: thad.getterman.org, /data/certs/thad.getterman.org/privkey.pem, /data/certs/thad.getterman.org/cert.pem, /data/certs/thad.getterman.org/fullchain.pem, /data/certs/thad.getterman.org/chain.pem
dehydrated | + Done!
dehydrated | RESULT
dehydrated | ------
dehydrated | True
dehydrated | + Requesting certificate...
dehydrated | + Checking certificate...
dehydrated | + Done!
dehydrated | + Creating fullchain.pem...
dehydrated | deploy_cert called: ltg.fyi, /data/certs/ltg.fyi/privkey.pem, /data/certs/ltg.fyi/cert.pem, /data/certs/ltg.fyi/fullchain.pem, /data/certs/ltg.fyi/chain.pem
dehydrated | + Done!
Press ^C
(CTRL-C) on your keyboard, and Docker Compose will exit and then remove the container. Assuming that everything worked correctly, now expunge the data in preparation for production use:
for currDir in accounts certs; do
rm -rfv "/srv/dehydrated/${currDir}/"*
done
In docker-compose.yaml
, disable dehydrated-testing.env
, and enable dehydrated-production.env
in its place.
Then, enable, start, and tail the Dehydrated systemd-based Docker Compose service:
{
systemctl enable docker-compose@dehydrated
systemctl start docker-compose@dehydrated
journalctl -f -u docker-compose@dehydrated
}
After several minutes, and similar to the prior messages from the testing server, you should see the domain certificates begin to populate. Press ^C
(CTRL-C) to exit tailing the systemd log for this service. This service will run indefinitely, and when a certificate needs to be renewed, will do so, and store it at /srv/dehydrated/certs
:
/srv/dehydrated/certs
├── ltg.fyi
│ ├── cert-0123456789.csr
│ ├── cert-0123456789.pem
│ ├── cert.csr -> cert-0123456789.csr
│ ├── cert.pem -> cert-0123456789.pem
│ ├── chain-0123456789.pem
│ ├── chain.pem -> chain-0123456789.pem
│ ├── combined.pem
│ ├── fullchain-0123456789.pem
│ ├── fullchain.pem -> fullchain-0123456789.pem
│ ├── privkey-0123456789.pem
│ └── privkey.pem -> privkey-0123456789.pem
└── thad.getterman.org
├── cert-0123456789.csr
├── cert-0123456789.pem
├── cert.csr -> cert-0123456789.csr
├── cert.pem -> cert-0123456789.pem
├── chain-0123456789.pem
├── chain.pem -> chain-0123456789.pem
├── combined.pem
├── fullchain-0123456789.pem
├── fullchain.pem -> fullchain-0123456789.pem
├── privkey-0123456789.pem
└── privkey.pem -> privkey-0123456789.pem
Distribution
Save the following script to /usr/local/bin/distro-certs.bash
#!/usr/bin/env bash
parallel-rsync \
--user=root \
--host 1.1.1.1 \
--host 2.2.2.2 \
--host 3.3.3.3 \
--host 4.4.4.4 \
--host 5.5.5.5 \
--host 6.6.6.6 \
--host 7.7.7.7 \
--host 8.8.8.8 \
--host 9.9.9.9 \
--extra-arg "--delete" \
--extra-arg "--no-owner" \
--extra-arg "--no-group" \
--extra-arg "--hard-links" \
--archive \
--recursive \
--compress \
"/srv/dehydrated/certs/" \
"/srv/letsencrypt/" \
;
Set up a Cron job for this script to run as you see fit with crontab -e
and use crontab guru to come up with a time that best suits you. Personally, I like to synchronize my certificates once per day:
24 12 * * * /usr/local/bin/distro-certs.bash > /var/log/distro-certs.log
Now our nine servers defined in the above script will have copies of these server certificates at /srv/letsencrypt/
which can be used by Traefik, Nginx, et al. — to see how I use this for Traefik, check out my next article, Traefik 2.5 quick-start guide.
Did this article save you time or money? I'd love a coffee!
Did you find this useful?
Please share with those who you believe would find this useful too!