How to use docker to generate wildcard SSL certificates for your website?

Google Chrome has started giving a warning for a non-SSL website and hence it has become more important than ever to generate SSL certificate for your website today!

When it comes to “docker” idea is simple, you mount a volume to share certificates with other containers. There are many docker images which have ‘in-built’ SSL generator. However, if you want it to be scalable, then this is a pretty bad way to do it. You would want to keep a track of all subdomains and their certificates along with where they have been generated. Load-balancers need not to be pointing to the “right” container during validation. So problems are many.

Docker image

I am using adferrand/letsencrypt-dns for this and it comes with ‘auto-restarting’ a docker container if a matching certificate has been renewed. It supports for 50+ dns managers and I am sure yours is covered 😉 . I am a fan of Linode, if you are serious about your business growth, give them a shot.
Docker Compose content:

cat docker-compose.yml

version: '3.2'
services:
  letsencrypt-dns:
    image: adferrand/letsencrypt-dns
    restart: always
    volumes:
        - "/etc/passwd:/etc/passwd:ro"
        - "/etc/group:/etc/group:ro"
        - "/var/run/docker.sock:/var/run/docker.sock"
        - "./letsencrypt:/etc/letsencrypt"
    environment:
        - CERTS_USER_OWNER=
        - CERTS_GROUP_OWNER=
        - CERTS_DIRS_MODE=0755
        - CERTS_FILES_MODE=0644
        - LETSENCRYPT_USER_MAIL=@.com
        - LEXICON_SLEEP_TIME=1500
        - LEXICON_PROVIDER=linode
        - LEXICON_LINODE_TOKEN=

Explaining the docker-compose.yml

We are mounting passwd and group as read-only to enable host user and group respectively.

Adding docker.sock ensures that it can restart related docker containers OR execute a command inside the targetted container. If you don’t mount it, containers will have old certificates even after certificates have been renewed and thus, it is very important that you mount it.

Since, dns server is using Linode’s DNS manager, we are adding LEXICON for linode and token. Sleep timing is 1500 seconds that means 25 mins, making each domain to be validated after 25 mins of adding the verification code in dns. If you are in USA, it will probably work with much lesser like 500 seconds.

Example content of domains.conf


cat letsencrypt/domains.conf

webapplicationconsultant.com *.webapplicationconsultant.com autorestart-containers=nginx_nginx_1,nginx_nginx_2
varunbatra.com *.varunbatra.com autocmd-containers=varunbatra_static_1:service nginx reload
  1. webapplicationconsultant.com *.webapplicationconsultant.com autorestart-containers=nginx_nginx_1,nginx_nginx_2 will restart containers by the name nginx_nginx_1 and nginx_nginx_2 once certificates of webapplicationconsultant.com has been renewed
  2. varunbatra.com *.varunbatra.com autocmd-containers=varunbatra_static_1:service nginx reload will execute the command service nginx reload once certificates of varunbatra.com have been renewed.

Generated SSL locations

  1. ./letsencrypt/live/varunbatra.com/fullchain.pem
  2. ./letsencrypt/live/webapplicationconsultant.com/fullchain.pem

Now you can use these certificates in NGINX or APACHE or ‘Whatever’ 🙂 Just make sure whatever you do, you don’t forget to add a proper autorestart and autocmd lines for their respective containers.

How to use docker to generate wildcard SSL certificates for your website?

22 thoughts on “How to use docker to generate wildcard SSL certificates for your website?

  1. If I’m using hosteurope dns provider how can I understand the value of linode token :
    – LEXICON_PROVIDER=hosteurope
    – LEXICON_LINODE_TOKEN= (what should be value – value of txt dns record or what ? )

    What If I want to have multiple differen wildcard domains, for example – *.example1.org , *.example2.org etc. , is this possible ?

  2. I want to implement for Digitalocean but it does not work

    letsencrypt-dns:
    image: adferrand/letsencrypt-dns
    restart: always
    volumes:
    – “/etc/passwd:/etc/passwd:ro”
    – “/etc/group:/etc/group:ro”
    – “/var/run/docker.sock:/var/run/docker.sock”
    – “./letsencrypt:/etc/letsencrypt”
    environment:
    – CERTS_USER_OWNER=
    – CERTS_GROUP_OWNER=
    – CERTS_DIRS_MODE=0755
    – CERTS_FILES_MODE=0644
    – LETSENCRYPT_USER_MAIL=@.com
    #- LETSENCRYPT_USER_MAIL=khristiam_r@hotmail.com
    – LEXICON_SLEEP_TIME=1500
    – LEXICON_PROVIDER=digitalocean
    – LEXICON_DIGITALOCEAN_TOKEN=
    # modo test
    – LETSENCRYPT_STAGING=true

    ==========================
    letsencrypt-dns_1 | chown: unknown group
    letsencrypt-dns_1 | 2019-06-05 00:27:22 circus[1] [INFO] Starting master on pid 1
    letsencrypt-dns_1 | 2019-06-05 00:27:22 circus[1] [INFO] Arbiter now waiting for commands
    letsencrypt-dns_1 | 2019-06-05 00:27:22 circus[1] [INFO] crond started
    letsencrypt-dns_1 | 2019-06-05 00:27:22 circus[1] [INFO] watch-domains started
    letsencrypt-dns_1 | 2019-06-05 00:27:22 [18] | #### Registering Let’s Encrypt account if needed ####
    letsencrypt-dns_1 | 2019-06-05 00:27:23 [18] | Saving debug log to /etc/letsencrypt/logs/letsencrypt.log
    letsencrypt-dns_1 | 2019-06-05 00:27:23 [18] | There is an existing account; registration of a duplicate account with this command is currently unsupported.
    letsencrypt-dns_1 | 2019-06-05 00:27:23 [18] | #### Clean autorestart/autocmd jobs
    letsencrypt-dns_1 | 2019-06-05 00:27:23 [18] | #### Creating missing certificates if needed (~1min for each) ####
    letsencrypt-dns_1 | 2019-06-05 00:27:23 [18] | >>> Creating a certificate for domain(s): -d *.unifser.com -d unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | Saving debug log to /etc/letsencrypt/logs/letsencrypt.log
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | Plugins selected: Authenticator manual, Installer None
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | Obtaining a new certificate
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | Performing the following challenges:
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | dns-01 challenge for unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | dns-01 challenge for unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:24 [18] | Running manual-auth-hook command: /var/lib/letsencrypt/hooks/authenticator.sh
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | manual-auth-hook command “/var/lib/letsencrypt/hooks/authenticator.sh” returned error code 1
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | Error output from manual-auth-hook command authenticator.sh:
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | Traceback (most recent call last):
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/bin/lexicon”, line 10, in
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | sys.exit(main())
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/cli.py”, line 117, in main
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | results = client.execute()
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/client.py”, line 71, in execute
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | self.provider.authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 69, in authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | return self._authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.py”, line 29, in _authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | self._get(‘/domains/{0}’.format(self.domain))
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 142, in _get
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | return self._request(‘GET’, url, query_params=query_params)
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.p
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | y”, line 146, in _request
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | response.raise_for_status()
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | File “/usr/local/lib/python3.7/site-packages/requests/models.py”, line 940, in raise_for_status
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | raise HTTPError(http_error_msg, response=self)
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.digitalocean.com/v2/domains/unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:25 [18] | Running manual-auth-hook command: /var/lib/letsencrypt/hooks/authenticator.sh
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | manual-auth-hook command “/var/lib/letsencrypt/hooks/authenticator.sh” returned error code 1
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | Error output from manual-auth-hook command authenticator.sh:
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | Traceback (most recent call last):
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/bin/lexicon”, line 10, in
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | sys.exit(main())
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/cli.py”, line 117, in main
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | results = client.execute()
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/client.py”, line 71, in execute
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | self.provider.authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 69, in authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | return self._authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.py”, line 29, in _authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | self._get(‘/domains/{0}’.format(self.domain))
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 142, in _get
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | return self._request(‘GET’, url, query_params=query_params)
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.p
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | y”, line 146, in _request
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | response.raise_for_status()
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | File “/usr/local/lib/python3.7/site-packages/requests/models.py”, line 940, in raise_for_status
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | raise HTTPError(http_error_msg, response=self)
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.digitalocean.com/v2/domains/unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:27 [18] | Waiting for verification…
    letsencrypt-dns_1 | 2019-06-05 00:27:28 [18] | Challenge failed for domain unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:28 [18] | Challenge failed for domain unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:28 [18] | dns-01 challenge for unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:28 [18] | dns-01 challenge for unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:28 [18] | Cleaning up challenges
    letsencrypt-dns_1 | 2019-06-05 00:27:28 [18] | Running manual-cleanup-hook command: /var/lib/letsencrypt/hooks/cleanup.sh
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | manual-cleanup-hook command “/var/lib/letsencrypt/hooks/cleanup.sh” returned error code 1
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | Error output from manual-cleanup-hook command cleanup.sh:
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | Traceback (most recent call last):
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/bin/lexicon”, line 10, in
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | sys.exit(main())
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/cli.py”, line 117, in main
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | results = client.execute()
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/client.py”, line 71, in execute
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | self.provider.authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 69, in authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | return self._authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.py”, line 29, in _authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | self._get(‘/domains/{0}’.format(self.domain))
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 142, in _get
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | return self._request(‘GET’, url, query_params=query_params)
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.py”, li
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | ne 146, in _request
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | response.raise_for_status()
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | File “/usr/local/lib/python3.7/site-packages/requests/models.py”, line 940, in raise_for_status
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | raise HTTPError(http_error_msg, response=self)
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.digitalocean.com/v2/domains/unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:29 [18] | Running manual-cleanup-hook command: /var/lib/letsencrypt/hooks/cleanup.sh
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | manual-cleanup-hook command “/var/lib/letsencrypt/hooks/cleanup.sh” returned error code 1
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Error output from manual-cleanup-hook command cleanup.sh:
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Traceback (most recent call last):
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/bin/lexicon”, line 10, in
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | sys.exit(main())
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/cli.py”, line 117, in main
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | results = client.execute()
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/client.py”, line 71, in execute
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | self.provider.authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 69, in authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | return self._authenticate()
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.py”, line 29, in _authenticate
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | self._get(‘/domains/{0}’.format(self.domain))
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/base.py”, line 142, in _get
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | return self._request(‘GET’, url, query_params=query_params)
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/lexicon/providers/digitalocean.py”, li
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | IMPORTANT NOTES:
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | – The following errors were reported by the server:
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Domain: unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Type: unauthorized
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Detail: No TXT record found at _acme-challenge.unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Domain: unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Type: unauthorized
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Detail: No TXT record found at _acme-challenge.unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | To fix these errors, please make sure that your domain name was
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | entered correctly and the DNS A/AAAA record(s) for that domain
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | contain(s) the right IP address.
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | ne 146, in _request
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | response.raise_for_status()
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | File “/usr/local/lib/python3.7/site-packages/requests/models.py”, line 940, in raise_for_status
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | raise HTTPError(http_error_msg, response=self)
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.digitalocean.com/v2/domains/unifser.com
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | Some challenges have failed.
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | md5sum: can’t open ‘/etc/letsencrypt/live/unifser.com/cert.pem’: No such file or directory
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | ### Revoke and delete certificates if needed ####
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | ### Reloading circusd configuration ###
    letsencrypt-dns_1 | 2019-06-05 00:27:30 [18] | ok

  3. I didn’t know this could be that easy. I have wasted a lot of time in screwing around with certbot non-wildcard certificates.

Leave a Reply

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

Scroll to top