I’m a bit sceptical when it comes to tools that magically do everything for you, especially if I already have an elaborated way for doing that stuff on my own. Let’s encrypt is one of those tools and I first thought, we will never become friends, but it seems that locking it in a docker container started a solid relationship.

For many years I searched for a way to get free SSL certificates for my web services. I tried out many providers but all of them had strong limitations. For example, StartSSL only provides one free certificate, CAcert’s CA is not part of all browsers nor will my own CA ever be. Furthermore all solutions required periodic manual intervention for updating the certificate.

On December 3rd, 2015 Let’s Encrypt announced to enter public beta.

Let’s Encrypt is a new Certificate Authority: It’s free, automated, and open.

It provides gratis certificates for all but in a new way: Instead of doing all the steps on your own you just execute a script on your server, and it does everything for you. It generates a private key, hooks into your webserver and validates if it is responsible for the domain, requests the certificate from a public server, installs it on your machine and configures the webserver to use it.

That’s all nice for people who are not familiar with SSL encryption and how it works. When you’re installing a SSL certifiate today, there are many options and you should be aware that some of them could really break all the security. Your encrypted connection could feel very secure while it uses old mechanisms that are known to have bugs and open up very severe security issues.

So, it’s great that there is a tool that does all the stuff for you, that configures your webserver in a highly secure way, and you don’t have to bother about all those parameters and options. When thinking about my own services, I don’t really like the idea, that some tool magically updates my configuration. My config files are years and years of work and I don’t want anyone to edit them except me.

That’s why Let’s encrypt was a no-go for my services at first sight, but the idea of always having an up-to-date certificate which is signed by a CA that is well known and trusted by several web clients made Let’s encrypt really attractive. So I decided to give it a try, and I’m really happy with the result.

Let’s have a look at my setup

With my solution letsencrypt is executed inside a docker container. The great thing with docker is, that I can decide what resources Let’s encrypt is allowed to access. The tool can do what it wants inside the container, but will never be able to do anything with my web services because it doesn’t see them. I love docker for that.


The only thing letsencrypt can see from inside the docker container is what I mount when running the container. In my case I allow access to the following directories:

  • /etc/letsencrypt
  • /var/lib/letsencrypt

For evaluating the domain, letsencrypt is started with a standalone webserver. This is useful when you don’t have a webserver on your server or if it is ok for you to temporarily stop an existing server, because letsencrypt will then bind to port 80 and 443 for communicating with the certificate authority.

This is not an option in my scenario, especially because I want to renew the certificates automatically, so this would mean a downtime for my webserver every month. So I let my existing nginx forward the requests to the standalone letsencrypt server inside the docker container. Therefore docker is also really useful, because I can tell docker to bind port 80 of the letsencrypt server to port 30080 of the docker container.

Complex setup in short: My nginx listens on port 80 and forwards some requests to port 30080 of the docker container, which then forwards the requests to port 80 of the standalone letsencrypt server. Same for 443.

Let’s get started

Since Let’s encrypt will not be installed on my system (instead it is installed on the system inside the docker container), I had to create the two directories first.

Then I wrote a bash script for running the docker container for a single domain:

In fact, the script is just a shortcut for the docker run command. The container is created using the following parameters:

  • -i and -t tell docker to run the container interactively and create a pseudo TTY
  • --rm tells docker to remove any existing container (so, for every run a new container will be created)
  • -p bind local ports to ports inside the container. This way we can access port 80 of the letsencrypt standalone server using port 30080 of our local machine.
  • -v bind directories of the local machine to directories inside the docker container. This enables letsencrypt to read and write the real directories of the server.
  • --name is just a name for the docker container

Additionally the following parameters are passed to letsencrypt:

  • auth Generate and authenticate the certificate using a standalone web server.
  • --server The Let’s encrypt server to use. By default my installation used the test server which signed certificates using a CA that is not part of the browsers CA list.
  • --agree-tos Agree the Terms of services of Let’s encrypt. Please read them before using this parameter!
  • --standalone-supported-challenges Use HTTP and not HTTPS for authenticating that the server is responsible for the domain. In fact that means that the binding for port 80 is never used. Check over HTTPS did not work for me.
  • $@ passes all arguments the script got from the command line to letsencrypt

I saved this file under /usr/local/bin/letsencrypt-docker, made it executable ( chmod +x ...) so I can simply run letsencrypt-docker --domain "site.example.com" --email "foo@example.com" to generate a certificate for site.example.com. But before doing this, I had to configure a proxy in nginx – otherwise Let’s encrypt won’t be able to access port 30080 – so here is a nginx configuration:

This is saved as /etc/nginx/conf/letsencrypt-http.conf and included in all server sections I wanted to generate a certificate for.

Now I could already generate the certificate using the above script but before doing this, I finished the SSL setup of the domains. Therefore I created another file for each domain that contains the SSL configuration for that domain. It could look as follows:

And finally, the server sections for the Let’s encrypt enabled domains look as follows:

For port 80 I include the /etc/nginx/conf/letsencrypt-http.conf and for port 443 I (later) include the /etc/nginx/ssl/site1.example.com.conf SSL configuration.

That’s it! Now I simply do the following steps:

  1. Run nginx reload to enable the new site on port 80 (and a wrong configuration on port 443).
  2. Run letsencrypt-docker --domain "site1.example.com" --email "foo@example.com" to get the certificate for your domain.
  3. Now uncomment the include line in the port 443 configuration.
  4. Run nginx reload again to enable the new site on port 443 with the new certificate too.

Let’s automate it

Certificates issued by Let’s encrypt have a lifetime of three months. That’s not much, so you have to execute the above script very often. But as mentioned, one thing that’s great about let’s encrypt is, that it doesn’t require manual intervention – simply achieved by a cron job.

Therefore I wrote another bash script, that simply calls letsencrypt-docker for every domain and reloads the nginx after all:

Finally I registered this as cron job that automatically runs every 15th of a month at 2:34 AM:

Let’s enjoy

I hope this post could help you with your own let letsencrypt-docker-nginx installation. If you have questions, don’t hesitate asking them below!

Update 2016-03-07

Fixed: cannot enable tty mode on non tty input
My scripts worked fine when they are executed from an interactive shell, but the did not work when executed with cron. This was because the cron session did not find the letsencrypt-docker-command and was not executed within an interactive shell, so docker exited with level=fatal msg="cannot enable tty mode on non tty input". To fix this, I removed the -t-switch from the docker command and added the full path to all letsencrypt-docker-commands in letsencrypt-docker-all.

Fixed: User chose to cancel the operation and may reinvoke the client.
This output told me, that the docker script required some user interaction. It asked something, but no one answered. So I assumed, the question was “do you really want to…”, so I added the parameter --renew-by-default to all lines in letsencrypt-docker-all.

Fixed: Missing output
The log file contained all the output of letsencrypt, but when successful, the letsencrypt command does not print the domains that have been updated. So I added this line:

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">