Companion service which adds Let’s Encrypt certificates to docker flow
Branch: master
Clone or download

Docker Flow: Let's Encrypt
GitHub release license Docker Pulls


This project is compatible with Viktor Farcic's Docker Flow: Proxy and Docker Flow: Swarm Listener. It uses certbot-auto to create and renew ssl certificates from Let’s Encrypt for your domains and stores them inside /etc/letsencrypt thus it requires a persistant storage. You can bind a folder from the host and use a constraint to make sure the companion service always runs on the same host or use storage plugins such as rexray to allow data to follow your container on other nodes.

The service setups a cron which runs by default two times a day (03:00 and 15:00 UTC) and calls renewAndSendToProxy. You can overwrite these cron behavior with the correct environment variables. It runs certbot-auto renew and uploads the certificates to the running Docker Flow: Proxy service.

You can find this project also on Docker Hub.

How does it work

This docker image uses certbot-auto, curl and cron to create and renew your Let’s Encrypt certificates. Through environment variables you set the domains certbot-auto should create certificates for, which e-mail is used by Let’s Encrypt when you lose the account and want to get it back, the cronjob starting times and the dns-name of Docker Flow: Proxy.

When the image starts, the script runs and creates/renews the certificates and creates /etc/cron.d/renewcron. The script also runs which combines the cert.pem, chain.pem and privkey.pem to a domainname.combined.pem file and uploads your cert via curl to your proxy. also calls certbot-auto renew because this script is run by default two times a day (03:00 and 15:00 UTC) via /etc/cron.d/renewcron. You can overwrite this behavior by changing CERTBOT_CRON_RENEW environment variable. This script also creates backups of the /etc/letsencrypt folder which are stored in /etc/letsencrypt/backup. Don't worry, the backup folder is excluded from the tar command.

As you can see the output is piped into /var/log/dockeroutput.log. This file is created in the Dockerfile and just redirects directly to the docker logs output. The logs are also colorized so that you are able to find the important information without hesitation.

If you only want to test this image you should add -e CERTBOTMODE="staging" when creating the service to use the staging mode of Let’s Encrypt. Remember that the certificate is not trusted so you will get a warning inside your browser.

Certbot-auto is called with --rsa-key-size 4096 --redirect --hsts --staple-ocsp for improved security.



docker build -t hamburml/docker-flow-letsencrypt .


Attention! If you use local storage, create the /etc/letsencrypt folder before you start the service.

docker service create --name letsencrypt-companion \
    --label com.df.notify=true \
    --label com.df.distribute=true \
    --label com.df.servicePath=/.well-known/acme-challenge \
    --label com.df.port=80 \
    -e DOMAIN_1="('' '' '')"\
    -e DOMAIN_2="('' '' '')"\
    -e CERTBOT_EMAIL="" \
    -e PROXY_ADDRESS="proxy" \
    -e CERTBOT_CRON_RENEW="('0 3 * * *' '0 15 * * *')"\
    --network proxy \
    --mount type=bind,source=/etc/letsencrypt,destination=/etc/letsencrypt \
    --constraint '<nodeId>' \
    --replicas 1 hamburml/docker-flow-letsencrypt:latest

The aclName label is optional. However it helps the proxy to order the ACls and make sure that the companion rule is above other service rules since the rules are added sequentially so ACME verification works.

You may also use a stack file to deploy the letsencrypt companion. The content of the docker-compose-stack.yml file is:

version: "3"


  # Let's Encrypt Companion
    image: hamburml/docker-flow-letsencrypt:latest
      - proxy
      - DOMAIN_1=('' '' '')
      - DOMAIN_2=('' '' '')
      - PROXY_ADDRESS=proxy
      - CERTBOT_CRON_RENEW=('0 3 * * *' '0 15 * * *')
          - /etc/letsencrypt:/etc/letsencrypt
        - com.df.servicePath=/.well-known/acme-challenge
        - com.df.notify=true
        - com.df.distribute=true
        - com.df.port=80
        constraints: [ == <nodeId>]
      replicas: 1

    external: true

As a full stack example with Viktor's proxy and listener, check the docker-compose-full-stack.yml file.

You should always start the service on the same docker host. You achieve this by setting to the id of the docker host on which the service should run. The nodeId can be get via docker node ls. You must not scale the service to two, this wouldn't make any sense! Only one instance of this companion should run. The certificates are only renewed when they are 60 days old or older. This is standard certbot-auto behavior. But the certificates will be renewed when you add subdomains to the domain-list! If you want to change the number of times certbot-auto renew and the publish script is called you need to change CERTBOT_CRON_RENEW. The syntax is described here.

Important: The first domain must always be the domain without any subdomains. That makes the folder-structure regular.

We need to obey Let’s Encrypt’s rate limits!

Docker Logs

You can see the progress of the running service through the logs.

root@server # docker logs letsencrypt-companion... -f

Docker Flow: Let's Encrypt started
We will use for certificate registration with certbot. This e-mail is used by Let's Encrypt when you lose the account and want to get it back.

Use certbot --standalone --non-interactive --expand --keep-until-expiring --agree-tos --standalone-supported-challenges http-01 --rsa-key-size 4096 --redirect --hsts --staple-ocsp   -d -d -d 
Certificate not yet due for renewal; no action taken.
(removed some entries...)
Docker Flow: Proxy DNS-Name: proxy
current folder name is:
concat certificates for
transmit to proxy
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  7114    0     0  100  7114      0   108k --:--:-- --:--:-- --:--:--  108k
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Date: Tue, 10 Jan 2017 18:54:43 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

proxy received
(removed some entries...)
Thanks for using Docker Flow: Let's Encrypt and have a nice day!

Starting supervisord (which starts and monitors cron)
(removed some entries...)

When you restart the service and the certificates can't be renewed the logs will show this also.

Certificate not yet due for renewal; no action taken.


Thanks for using docker-flow-letsencrypt. If you have problems or some ideas how this can be made better feel free to create a new issue. Thanks to Viktor Farcic for his help and docker flow :)