Using the Let's Encrypt docker image to create and renew an SSL certificate for an nginx web site

Updated - Dec 10 2016 - thanks goes out to our reader Philippe for helpful feedback!

Let’s Encrypt is a free, automated, and open certificate authority (CA), run for the public’s benefit. Let’s Encrypt is a service provided by the Internet Security Research Group. This is a fantastic service that allows you to enable HTTPS for your websites and web applications without the cost and overhead of using a commercial certificate authority. Certificates issued by Let's Encrypt are valid for 90 days, so you should take care to automate the renewal process.

In this article we will use the official Let's Encrypt client from quay.io to:

  • generate a certificate for a website
  • perform a dry run of the renewal process
  • perform a renewal of the certificate

The process described in this article does not require that you interrupt the availability of your website when performing the renewal. However, you will need to reload the nginx configuration in order to pick up the new certificates once they are renewed.

Step 1: Generate a certificate using the official Let's Encrypt client

To complete this step, you should have a server with the docker engine installed. As well, you will need to have configured your DNS records for the domain that you want to secure. If your domain is test.example.com you should have an A record for test.example.com pointing to the IP address of your server.

Before performing this step ensure that there are no other containers or services listening on ports 80 or 443.

Connect to the server via SSH and from the command prompt enter the following command:

> sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \
        -v "/etc/letsencrypt:/etc/letsencrypt" \
        -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
        quay.io/letsencrypt/letsencrypt:latest certonly

This will run the Let's Encrypt client inside of a docker container with the following arguments:

  • -it run the container interactively with a pseudo-tty
  • --rm remove the container after it is finished running
  • -p publish ports 443 and 80
  • -v maps the /etc/letsencrypt and /var/lib/letsencrypt directories
  • certonly run the certonly tool

When you run this command the Let's Encrypt certonly client will start up. Enter the following information:

  • your email address
  • option 2 (standalone)
  • the domain (e.g. test.example.com)

When this command is completed, your certificates can be found in the directory /etc/letsencrypt/live/test.example.com (this path depends on the domain name provided). In this directory you will find the files:

  • cert.pem
  • chain.pem
  • fullchain.pem
  • privkey.pem

These four files are actually softlinks pointing to the actual current certificate and key files located in /etc/letsencrypt/archive/test.example.com/. When a certificate is renewed, the softlinks will be update appropriately.

Step 2: Running an Nginx webserver with the certificate

In this step, we will start up a new container running nginx with default.conf file for nginx to pick up the certificate files and handle HTTPS requests.

To use the certificates that we generated in step 1, create the following configuration file called default.conf

server {
    listen       80;

    server_name test.example.com;

    location /.well-known {
        proxy_pass http://127.0.0.1:8080/;
    }

    return 301 https://$server_name$request_uri;
}

server {
    listen       443 ssl;
    server_name  test.example.com;

    ssl_certificate      /etc/letsencrypt/live/test.example.com/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/test.example.com/privkey.pem;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    root /usr/share/nginx/html/;

    location /.well-known {
        proxy_pass http://127.0.0.1:4343/;
    }

}

Make sure that you are using your actual domain name (rather than test.example.com) in the paths specified in the directives:

ssl_certificate      /etc/letsencrypt/live/test.example.com/fullchain.pem;
ssl_certificate_key  /etc/letsencrypt/live/test.example.com/privkey.pem;

Next, we'll create and nginx web server with the arguments:

> sudo docker run -d -p 80:80 -p 443:443 --name testssl_web \
    -v /etc/letsencrypt/live/test.example.com/:/etc/letsencrypt/live/test.example.com/ \
    -v /etc/letsencrypt/archive/test.example.com/:/etc/letsencrypt/archive/test.example.com/ nginx

In this command, we've mapped volumes for /etc/letsencrypt/live/test.example.com (make sure this last part is your domain name rather than test.example.com).

Finally we will copy the default.conf file into the container using the docker cp command and reload the nginx configuration file (assuming you've uploaded the default.conf to /home/core on your server).

> sudo docker cp /home/core/default.conf testssl_web:/etc/nginx/conf.d/default.conf
> sudo docker exec testssl_web service nginx reload

After running this command you should be able to browse to your domain and see the nginx test page and see that it has redirected automatically to HTTPS and is using the certificate you created in step 1!

Step 3: Testing a dry-run of the renewal process

You may have noticed the following directives in the default.conf file:

location /.well-known {
    proxy_pass http://127.0.0.1:8080/;
}

and

location /.well-known {
    proxy_pass http://127.0.0.1:4343/;
}

These directives proxy requests to http(s)://test.example.com to a localhost resource listening on ports 8080 and 4343 respectively. These will be used in the renewal process. During the renewal process, the Let's Encrypt client will create a resource under the .well-known directory in the web root so that the Let's Encrypt server can authenticate the renewal. What we need to do here is have the Let's Encrypt client map the container ports 80 and 443 to the host ports 8080 and 4343 respectively. This is accomplished with the following command:

> sudo docker run -it --rm -p 4343:443 -p 8080:80 --name certbot \
            -v "/etc/letsencrypt:/etc/letsencrypt" \
            -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
            quay.io/letsencrypt/letsencrypt:latest renew --dry-run

When running this command, you will see output that looks like this:

-------------------------------------------------------------------------------
Processing /etc/letsencrypt/renewal/test.example.com.conf
-------------------------------------------------------------------------------
Cert not due for renewal, but simulating renewal for dry run
Starting new HTTPS connection (1): acme-staging.api.letsencrypt.org
Renewing an existing certificate
Performing the following challenges:
tls-sni-01 challenge for test.example.com
Waiting for verification...
Cleaning up challenges
Generating key (2048 bits): /etc/letsencrypt/keys/0011_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0011_csr-certbot.pem
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/test.example.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)

This indicates that the Let's Encrypt client was able to successfully perform a dry-run of your renewal.

Step 4: Performing a certificate renewal

With the dry run successfully tested, let's run an actual renewal. Since the certificate we are renewing was just created, we will need to use the --force-renew option. When performing a renewal, Let's Encrypt will check the expiration of the certificate. If the expiration date is more than 30 days in the future, it will skip the renewal. We can force a renewal with the aptly named --force-renew option.

Use the following command:

> sudo docker run -it --rm -p 4343:443 -p 8080:80 --name certbot \
            -v "/etc/letsencrypt:/etc/letsencrypt" \
            -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
            quay.io/letsencrypt/letsencrypt:latest renew --force-renew

After running this command, you will have an updated certificate. To have your nginx web server pick up the change, execute the command:

> sudo docker exec testssl_web service nginx reload

After the nginx server has reloaded, you can browse to your domain and it will have picked up the renewed certificate!

Conclusion

This is a great way to cost-effectively secure your web sites and applications. An added bonus of using Let's Encrypt is not having to wait for a third-party service provider to process and turn around your certificate requests and renewals. After ensuring that the dry-run renewal outputs a success message, you can run the commands:

> sudo docker run -it --rm -p 4343:443 -p 8080:80 --name certbot \
        -v "/etc/letsencrypt:/etc/letsencrypt" \
        -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
        quay.io/letsencrypt/letsencrypt:latest renew

and

> sudo docker exec testssl_web service nginx reload

You can automate this further by creating a cron job or a systemd timer that runs daily, weekly or monthly.