Using certbot (via Docker!) to autogenerate SSL certificates
Image credits: https://www.eff.org/pages/certbot
Folks that know me also know that I'm a big fan of the work of the Electronic Foundier Foundation.
Today I'll give you one more reason to love (or discover) them even if you never cared about their battles on digital privacy, free speech, etc..
If, like me, you have a website and you have been regenerating your SSL certificates by hand every year like a peasant (while also paying an SSL provider for them!) you might enjoy this small quality of life improvement given by certbot.
For me, it's literally been LIFE-CHANGING: it doesn't happen everyday that you discover how to save money and time on what is fundamentally a chore!
Certbot
The core tool is the certbot CLI written by the EFF.
There is a great guide on how to use it at https://eff-certbot.readthedocs.io/en/stable. It's very exhaustive (thus pretty long), so I must thank https://ecostack.dev/posts/nginx-lets-encrypt-certificate-https-docker-compose for covering how to neatly integrate things with a Docker setup. Here I'll most refer to the "Webroot" flow, also covered here: https://eff-certbot.readthedocs.io/en/stable/using.html#webroot.
So what's the value of writing my own article on this? As usual, I mostly take great work done by other folks and tailor it to my needs. Also, just in case other websites go down, I like to have my own self-hosted source of knowledge for things I find useful! :)
Nginx
Let's assume that you're using NGINX in a classic Docker Compose setup, like this:
services:
nginx:
build:
context: ./nginx
dockerfile: Dockerfile.prod
ports:
- 80:80
- 443:443
volumes:
- ./nginx/var/www:/var/www
- ./nginx/ssl:/etc/nginx/certs
You can create a new dedicated service that uses certbots official Docker image:
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/var/:/var/certbot/:rw
- ./certbot/etc/:/etc/letsencrypt/:rw
/etc/letsencryptis where the final certs will live (as generated bycertbot)/var/certbotcontains configuration files used internally bycertbot(like the auth challenge)
So we'll add those 2 extra volumes to our Nginx service as well:
- ./certbot/var/:/var/certbot/:ro
- ./certbot/etc/:/etc/letsencrypt/:ro
The final Compose file might look like this:
services:
nginx:
user: root
build:
context: ./nginx
dockerfile: Dockerfile.prod
ports:
- 80:80
- 443:443
volumes:
- ./nginx/var/www:/var/www
- ./certbot/var/:/var/certbot/:ro
- ./certbot/etc/:/etc/letsencrypt/:ro
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/var/:/var/certbot/:rw
- ./certbot/etc/:/etc/letsencrypt/:rw
We'll also want to tell Nginx to serve a certain "well known" directory that will be used by certbot for the auth challenge.
For example, this is what I've added to my nginx.conf:
# Certbot
location /.well-known/acme-challenge {
root /var/certbot;
}
Note: If you keep your website under source control, you'll probably want to run
mkdir -p certbot/{etc,www},touch certbot/{etc,www}/.gitkeepand commit those 2 .gitkeep files so that when you pull the changes on your target host those directories already exist!
Certificate generation
Let's get to the meat of it: generating the certs (and solving the related auth challenge) using just certbot.
For that, you can write a run-certbot.sh bash script with this content:
#!/usr/bin/env bash
dryrun=''
if [ "$1" == '--dry-run' ]; then
dryrun="$1"
fi
docker compose -f prod.yml \
run --rm \
certbot certonly \
--webroot \
--webroot-path /var/certbot/ \
$dryrun -d [your-domain-here]
Note that this script assumes that your compose file is called prod.yml, so re-adapt it as needed.
At this point you'll want to ssh to the host where you have your website running (assuming it's a VPS of sorts), pull these new changes we made to the compose and Nginx files and restart the service.
Note that we'll only need to boot up our Nginx service so that it serves the newly added location, which we'll use to pass the auth challenge.
So, for example, you'd run: docker compose -f prod.yml up --build --detach nginx.
Note: we're not running just
docker compose -f prod.yml up --build --detachsince we don't need to start thecertbotservice!
Now we can finally run ./run-certbot.sh, which will generate the final certificates inside the /etc/letsencrypt directory in the container. You can also run it in dry run mode, so without generating any files: ./run-certbot.sh --dry-run.
You'll be prompted to answer a few questions by Certbot: answer as needed.
Mostly, it's important that you'll provide the correct domain name of your website.
If everything ran correctly, at this point you'll have generated all the certs that Nginx needs! For example, in my case:
$ ls -1 certbot/etc/live/[my-domain-name]
cert.pem
chain.pem
fullchain.pem
privkey.pem
README
Et voilá, may certbot be blessed!
Now you just need to update your Nginx conf to point to these new certificates! For example:
ssl_certificate /etc/letsencrypt/live/[your-domain-here]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[your-domain-here]/privkey.pem;Renewal
The beauty of certbot is that it completely removes the need to manually reissue certificates.
Docs on this aspect are here: https://eff-certbot.readthedocs.io/en/latest/using.html#setting-up-automated-renewal.
For example, to constantly regenerate certs in the background, you could setup something like this:
certbot-renew:
build:
context: ./certbot-renew
dockerfile: Dockerfile
volumes:
- ./certbot/var/:/var/certbot/:rw
- ./certbot/etc/:/etc/letsencrypt/:rw
and then, as the ./certbot-renew/Dockerfile:
FROM certbot/certbot:latest
COPY ./renew.sh .
ENTRYPOINT ["/bin/sh"]
CMD ["renew.sh"]
and as ./certbot-renew/renew.sh:
#!/usr/bin/env sh
set -x
while true; do
certbot renew --webroot --webroot-path /var/certbot
sleep 12h
done
You'd then run this container by itself:
docker compose -f prod.yml up --build --detach certbot-renew
If everything goes well, this would print something like this:
[ ..redacted.. ]
certbot-renew-1 | + true
certbot-renew-1 | + certbot renew --webroot --webroot-path /var/certbot
certbot-renew-1 | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot-renew-1 |
certbot-renew-1 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot-renew-1 | Processing /etc/letsencrypt/renewal/[your-domain].conf
certbot-renew-1 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot-renew-1 | Certificate not yet due for renewal
certbot-renew-1 |
certbot-renew-1 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot-renew-1 | The following certificates are not due for renewal yet:
certbot-renew-1 | /etc/letsencrypt/live/[your-domain]/fullchain.pem expires on 2026-05-21 (skipped)
certbot-renew-1 | No renewals were attempted.
certbot-renew-1 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot-renew-1 | + sleep 12h
Of course you could also use just a cronjob here to run it twice a day instead of a basic while while true, but you get the gist.
Happy encrypting! 🔒