Using Let's Encrypt Certbot from Docker

Get Let's Encrypt TLS certificates without installing Snap

·

10 min read

Using Let's Encrypt Certbot from Docker

1. Introduction

I first came across letsencrypt when I was looking for free TLS domain certificates. The process to obtain and install letsencrypt certificates seemed a bit unusual to me at first compared to the ones I used before. Initially, I found it challenging, but after using it for a while, I came to realize that their process makes it easier to automatically create and renew certificates.

The process I was familiar with before using letsencrypt involves the user generating a CSR (certificate signing request) and then uploading it to the trust authority's website. The website then gives the user a small token, and it asks them to host it on the user's webserver at a specific URL. When the user does that, the trust authority will be able to verify that the user is indeed the owner of the domains in the CSR, so it generates the certificate and allows the user to download it. The user subsequently takes that certificate and installs it on the applications/servers it is intended for. As you can tell, that is a lot of manual work!

2. Certbot

When you use letsencrypt, there is a similar process, except that the user is replaced with a bot! Or as it is officially named,certbot. The challenge I faced with certbot at the time was that the only officially supported way to install the most recent version was through a package manager called Snap, which does not come pre-installed on the distributions I use. There are also some other package managers now that are supported on a best-effort basis such as pip.

After some research, I found that they provide a Docker image for certbot. I decided to use the Docker image instead of introducing Snap to the environment and the pipelines, since my web application is already based on Docker. I found that this approach has its advantages and its disadvantages, and I will try to cover both.

3. Common prerequisites

In the two approaches described in this tutorial, it is assumed that

  1. your domain(s) point to your host through the appropriate DNS records,
  2. you have root access to the host,
  3. Docker is already installed on the host,
  4. and the host can connect to letsencrypt servers (try running telnet letsencrypt.org 443).

4. Common variables

Some variables will be reused in multiple commands, so customize these variables according to your deployment and store them in a file named env.sh.

# The is a name of your choice.
# It will be assigned to the certificate upon creation, and it will be used later for renewals as well.
export DOMAIN_CERT_NAME=example_com

export DOMAINS_LIST="--domain example.com --domain www.example.com"

# The email to receive renewal reminders
export DOMAIN_MAINTAINER=me@example.com

# This could be /var/www/ if the webserver is running on the host directly,
# or it could be a path to the Docker volume that maps to the webroot of the container.
# This variable is used for the webroot approach only, and there is more information about it in its section    
export DOMAIN_WEBROOT=/tmp/www/

# This is where certbot will store the generated files including the certificates.
# It is in the /tmp/ partition this demo, but you should use a different partition in an actual deployment
export LETSENCRYPT_PATH="/tmp/letsencrypt"

5. Creating and renewing certificates

There are few certbot plugins (aka subcommands) to obtain and install the certificates. The correct plugin to use depends on your deployment and your use case. All the certbot plugins are documented briefly here. It is important that you check the documentation so that you choose the correct plugin for your use case. In this tutorial, we will see the webroot and the standalone plugins, mainly because they are generic and work well for small projects. If you choose another plugin, the commands shown here can still work with some modifications.

Note that the --force-renewal parameter that you will see in the next commands is used for testing. After you ensure that the commands are working as expected, you can remove that parameter so that the certificate gets renewed only when it is about to expire. By removing that parameter, you can schedule the command to run everyday, but the renewal will take place only when 30 days are remaining before expiration.

5.1 The certbot webroot plugin

In many patterns of web deployments, it is common to have a webserver listening on port 443 or 80, where it serves content from multiple sources including the web root directory (e.g., /var/www/). If the the Unix user that will run the certbot container has write access to the web root directory (either directly or through a Docker volume if the webserver is containerized), then the webroot plugin is probably the way to go.

If you do not already have such webserver in your deployment, then check the standalone approach.

5.1.1 Dummy webserver

For this tutorial, I will work with a dummy webserver that acts as a stand-in for your existing webserver we talked about. Here is the command to create it.

source env.sh

docker run \
    --name mywebserver \
    -p 80:80 \
    -v "$(readlink -f "$DOMAIN_WEBROOT"):/usr/share/nginx/html" \
    -d nginx

After running the above command, you will find that a directory has been created on the host at $DOMAIN_WEBROOT. Now create an index.html file in that directory and ensure that you can view it at yourdomain:80.

5.1.2 Creating a certificate for the first time (webroot)

All you need now to create the certificate is your environment variables and this lengthy command. Note that in this command, the /html and the /etc/letsencrypt strings should not be customized. They are internal paths inside the temporary cerbot container and are already mapped to the custom variables $DOMAIN_WEBROOT and $LETSENCRYPT_PATH.

source env.sh

docker run \
  -it \
  --rm \
  --volume $(readlink -f "$DOMAIN_WEBROOT"):/html \
  --volume $(readlink -f "$LETSENCRYPT_PATH"):/etc/letsencrypt \
  certbot/certbot \
    certonly \
    --verbose \
    --noninteractive \
    --agree-tos \
    --cert-name "$DOMAIN_CERT_NAME" \
    --email "$DOMAIN_MAINTAINER" \
    $DOMAINS_LIST \
    --webroot \
    --webroot-path /html

If you get an output similar to what is below, then congratulations! You obtained the certificate.

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Account registered.
Requesting a certificate for example.com and www.example.com
Performing the following challenges:
http-01 challenge for example.com
http-01 challenge for www.example.com
Using the webroot path /html for all unmatched domains.
Waiting for verification...
Cleaning up challenges

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example_com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example_com/privkey.pem
This certificate expires on 2022-10-28.
These files will be updated when the certificate renews.

NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The certificate files will be available on the host at $LETSENCRYPT_PATH/live/$DOMAIN_CERT_NAME. You can point your applications/webservers to read (or preferably copy) the certificate from this path. If they are containerized, you can mount the $LETSENCRYPT_PATH directory in the containers like we did with the certbot container.

$ source env.sh
$ sudo ls -la $LETSENCRYPT_PATH/live/$DOMAIN_CERT_NAME
total 12
drwxr-xr-x 2 root root 4096 Jul 30 05:04 .
drwx------ 3 root root 4096 Jul 30 04:59 ..
lrwxrwxrwx 1 root root   45 Jul 30 05:04 cert.pem -> ../../archive/example_com/cert1.pem
lrwxrwxrwx 1 root root   46 Jul 30 05:04 chain.pem -> ../../archive/example_com/chain1.pem
lrwxrwxrwx 1 root root   50 Jul 30 05:04 fullchain.pem -> ../../archive/example_com/fullchain1.pem
lrwxrwxrwx 1 root root   48 Jul 30 05:04 privkey.pem -> ../../archive/example_com/privkey1.pem
-rw-r--r-- 1 root root  692 Jul 30 04:59 README

5.1.3 Renewing an existing certificate (webroot)

Now let's see how to renew the certificate. You can either run this command manually, or you can schedule it to run periodically using something like crontab. Don't forget to remove the --force-renewal parameter before you schedule it so you don't risk exceeding the fair usage limits.

source env.sh

docker run \
  -it \
  --rm \
  --volume $(readlink -f "$DOMAIN_WEBROOT"):/html \
  --volume $(readlink -f "$LETSENCRYPT_PATH"):/etc/letsencrypt \
  certbot/certbot \
    renew \
     --force-renewal \
     --noninteractive \
     --agree-tos \
     --cert-name "$DOMAIN_CERT_NAME" \
     --webroot \
     --webroot-path /html

If the update is succesful, you will get an output similar to this.

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/example_com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewing an existing certificate for example.com and www.example.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all renewals succeeded: 
  /etc/letsencrypt/live/example_com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

5.2 The certbot standalone plugin

In the standalone mode, certbot will bind to port 80 so that it can serve the verification tokens. This approach assumes that no other processes are listening to port 80. The commands with the standalone plugin are very similar to those we used with the webroot plugin. The only differences are that we map port 80 between the host and the container, we repace --webroot with --standalone, and we remove the --webroot-path /html parameter.

5.2.1 Creating a certificate for the first time (standalone)

This is the command to obtain a certificate for the first time using the standalone plugin.

source env.sh

docker run \
  -it \
  --rm \
  --volume $(readlink -f "$DOMAIN_WEBROOT"):/html \
  --volume $(readlink -f "$LETSENCRYPT_PATH"):/etc/letsencrypt \
  --publish 80:80 \
  certbot/certbot \
    certonly \
    --verbose \
    --noninteractive \
    --agree-tos \
    --cert-name "$DOMAIN_CERT_NAME" \
    --email "$DOMAIN_MAINTAINER" \
    $DOMAINS_LIST \
    --standalone

The log output will be identical to that of the webroot plugin we have seen before, except for the following line.

Plugins selected: Authenticator standalone, Installer None

And also as we have seen with the webroot plugin, the certificate files will be at $LETSENCRYPT_PATH/live/$DOMAIN_CERT_NAME.

5.2.2 Renewing an existing certificate (standalone)

This is the renewal command using the standalone plugin. The log output will be identical to that of the webroot plugin we have seen before.

source env.sh

docker run \
  -it \
  --rm \
  --volume $(readlink -f "$DOMAIN_WEBROOT"):/html \
  --volume $(readlink -f "$LETSENCRYPT_PATH"):/etc/letsencrypt \
  --publish 80:80 \
  certbot/certbot \
    renew \
     --force-renewal \
     --noninteractive \
     --agree-tos \
     --cert-name "$DOMAIN_CERT_NAME" \
     --standalone

6. Disadvantages

With the renew command, it is possible to execute scripts before and after the renewal. For example, using the webroot plugin, the user may want to restart a container after the renewal. In that case, the user can add --deploy-hook "docker restart mywebserver" to the renew command. The hook will be executed only when the certificate is successfully renewed, so the container will not be restarted unnecessarily.

The main disadvantage of using a containerized certbot in my opinion is that the renewal hooks will be sandboxed. They will not be able to restart services on the host, at least without some ugly hacks. To work around this, the best (although not ideal) approach I see so far is to have my renewal script check for the certificate files before and after executing the renew command. If the files change, the script restarts the required services.

7. References