Using Let's Encrypt with Supervisor
Learn how to use Supervisor to automate Let's Encrypt certificates in a Docker-friendly way
Let's Encrypt with Supervisor
Let's Encrypt works great with Supervisor, as it provides easy orchestration and some basic scheduling that the certificate management requires. Configuring it is also not rocket science; just identify the environment your app is running in, and choose a suitable workflow. This can even be done with minimal changes to your app, and in a Docker-friendly way.
For automating Let's Encrypt, you generally need 3 program:
- The app itself (Apache, Node.js, or whatever you use)
- An initial cert issuance script. This will run when you start the server
- A renewal script that handles getting new certificates
A basic supervisord.conf
which we'll customize looks like this:
[supervisord]
nodaemon=true
[program:app]
command=...start
[program:letsencrypt]
command=/bin/bash -c "certbot certonly --keep-until-expiring ...other parameters"
autorestart=false
[program:letsencrypt-renew]
command=/bin/bash -c "sleep 1d && certbot renew ...other parameters"
autostart=false
autorestart=true
Depending on the authorization flow you use, the exact parameters and when each program start/stop differ.
The app is to be managed by supervisor. This is usually the case, but for example Apache spawns child processes
and that makes it out of the scope of supervisor. Make sure that supervisorctl restart app
and supervisorctl stop app
works.
The letsencrypt program handles the initial certificate. It does not autorestart, as it is run only once. The
--keep-until-expiring
flag makes sure that if there is an existing certificate, it will use that instead of
getting a new one.
And finally, letsencrypt-renew runs every day and does the renewals. It autorestarts, but does not autostarts, so it needs a kickoff.
Flow #1: Get the certificate before launching the app, shut down during renewal
This is the easiest case. The app runs neither during getting a certificate, nor when a renewal is happening.
Part #1: run letsencrypt then start app
The only program that autostarts is the letsencrypt, and it starts both the app and the letsencrypt-renew.
[program: app]
...
autostart=false
[program:letsencrypt]
command=/bin/bash -c "certbot certonly --keep-until-expiring --standalone --agree-tos ... --staging -d $CERT_URL && supervisorctl start app letsencrypt-renew"
...
Do not use --post-hook
or --deploy-hook
, as they won't run if a valid certificate already exists. Instead, use the
... && ...
construct, that runs every time after the program is finished.
Part #2: Stop app -> renew -> start app
The other part is to configure how to renew the certificate. In this case, during renewal, the app is not running. To achieve
this, use the --pre-hook
and --post-hook
.
[program:letsencrypt-renew]
command=/bin/bash -c "sleep 1d && certbot renew --pre-hook 'supervisorctl stop app' --post-hook 'supervisorctl start app'"
...
Since it was not defined, renewal also uses the standalone auth.
Flow #2: Start the app, restart on new certificate
If you can configure your app so that Let's Encrypt can use the webroot auth, then you can start it and only need to restart on new certificates.
Make sure you handle the case when you have no valid certificates, for example, use a self-signed one.
In this case, app and letsencrypt autostarts.
[program: app]
...
autostart=true
[program:letsencrypt]
command=/bin/bash -c "certbot certonly --keep-until-expiring ... --staging -d $CERT_URL --deploy-hook 'supervisorctl restart app' && supervisorctl start letsencrypt-renew"
autostart=true
[program:letsencrypt-renew]
command=/bin/bash -c "sleep 1d && certbot renew"
autostart=false
...
Use --deploy-hook
to restart the app, as most server software won't pick up a new certificate automatically. And since
the renewal flow works the same, it does not need any parameters.
There is one problem with this setup. A race condition is present between the app and letsencryt, as the latter
needs the former to run. If your app needs more time to start serving static files, you need to start letsencrypt
after that with supervisorctl start letsencrypt
instead of autostarting it.
Another solution is to use a sleep
before letsencrypt, like sleep 5m && ...
. But that hardcodes an upper
limit, which is fragile.
A better solution is to use a script that waits for the server to start, like the wait-on that checks a HTTP service:
command=/bin/bash -c "wait-on https://app/health-check && certbot certonly ... "
This will ping the parameter URL and only run letsencrypt when it is alive.