One of the problems I’ve been facing lately was to create a service that was served by SSL/TLS protocol. Most of the guides that can be found online show you some simple steps of installing a service without HTTPS listening in port 80 and go no further. For this reason,, is that I came up with this guide on how to serve a service through nginx that is served through HTTPS and that certificates are managed by Let’s Encrypt.
Let’s Encypt’s Certbot in a Docker Container
Before we can execute the Certbot command that installs a new certificate, we need to run a very basic instance of Nginx so that our domain is accessible over HTTP.
In order for Let’s Encrypt to issue you a certificate, an ACME Challenge Request is performed:
- You issue a command to the Certbot agent
- Certbot informs Let’s Encrypt that you want an SSL/TLS certificate
- Let’s Encrypt sends the Certbot agent a unique token
- The Certbot agent places the token at an endpoint on your domain that looks like: http://{domain}/.well-known/acme-challenge/{token}
- If the token at the endpoint matches the token that was sent to the Certbot agent from the Let’s Encrypt CA, the challenge request was successful and Let’s Encrypt knows that you are in control of the domain.
This basic instance of Nginx will only ever be run for the first time that you request a certificate from Let’s Encrypt. It’s a basic instance because it doesn’t even need to have a default page. It just needs to give write permissions to the Certbot agent so that it can place a token at an endpoint for the challenge request and that’s all.
We can’t configure a single instance of Nginx because the first instance of Nginx will only be configured for HTTP since we do not have an SSL/TLS certificate yet. Once we have the SSL/TLS certificate, we can configure SSL/TLS on the full production version of the site. If we then need to renew a certificate between 60 and 90 days after the first certificate was issued, the subsequent challenge requests will be performed on the production version of our site running on Nginx, and so we won’t ever have to run the basic instance of Nginx again.
Obtaining the Let’s Encrypt SSL/TLS Certificate
We need to create a docker compose that does the following:
- Pulls the latest version of Nginx from the Docker registry
- Exposes port 80 on the container to port 80 on the host, which means that requests to your domain on port 80 will be forwarded to nginx running in the Docker container
- Maps the nginx configuration file that we will create in the next step to the configuration location in the Nginx container. When the container starts, it will load our custom configuration
- Maps the Let’s Encrypt location to the default location of Nginx in the container.
- Creates a default Docker network
# docker-compose.yml
services:
letsencrypt-nginx-container:
container\_name: 'letsencrypt-nginx-container'
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- docker-network
networks:
docker-network:
driver: bridge
Then, create the configuration file for nginx
# nginx.conf
server {
listen 80;
listen [::]:80;
server\_name {domains};
location ~ /.well-known/acme-challenge {
allow all;
root /usr/share/nginx/html;
}
root /usr/share/nginx/html;
index index.html;
}
The nginx configuration file does the following:
- Listens for requests on port 80 for URLs in the domain
- Gives the Certbot agent access to ./well-known/acme-challenge
- Sets the default root and file
Before running the Certbot command, spin up a Nginx container in Docker to ensure the temporary Nginx site is up and running
sudo docker-compose up -d
Then, open up a browser and visit the domain to ensure that the Docker container is up and running and accessible.
We’re almost ready to execute the Certbot command. But before we do, you need to be aware that Let’s Encrypt has rate limits. Most notably, there’s a limit of 20 issued certificates per 7 days. So if you exceeded 20 requests and are having a problem with generating your certificate for whatever reason, you could run into trouble. Therefore, it’s always wise to run your commands with a — staging parameter which will allow you to test if your commands will execute properly before running the actual commands.
Run the staging command for issuing a new certificate:
sudo docker run -it --rm \
-v /docker-volumes/etc/letsencrypt:/etc/letsencrypt \
-v /docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /docker/letsencrypt-docker-nginx/src/letsencrypt/letsencrypt-site:/data/letsencrypt \
-v "/docker-volumes/var/log/letsencrypt:/var/log/letsencrypt" \
certbot/certbot \
certonly --webroot \
--register-unsafely-without-email --agree-tos \
--webroot-path=/data/letsencrypt \
--staging \
-d {domain}
Issue a new Let’s Encrypt Certificate with Certbot and Docker in Staging Mode
The command does the following:
- Run docker in interactive mode so that the output is visible in terminal
- If the process is finished close, stop and remove the container
- Map 4 volumes from the server to the Certbot Docker Container:
- The Let’s Encrypt Folder where the certificates will be saved
- Lib folder
- Map our html and other pages in our site folder to the data folder that let’s encrypt will use for challenges.
- Map a logging path for possible troubleshooting if needed
- For staging, we’re not specifying an email address
- We agree to terms of service
- Specify the webroot path
- Run as staging
- Issue the certificate to be valid for the A record and the CNAME record
You can also get some additional information about certificates for your domain by running the Certbot certificates command:
sudo docker run --rm -it --name certbot \
-v /docker-volumes/etc/letsencrypt:/etc/letsencrypt \
-v /docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /docker/letsencrypt-docker-nginx/src/letsencrypt/letsencrypt-site:/data/letsencrypt \
certbot/certbot \
--staging \
certificates
Get Additional Information with the Certbot Certificates Command
If the staging command executed successfully, execute the command to return a live certificate
First, clean up staging artifacts:
sudo rm -rf /docker-volumes/
And then request a production certificate: (note that it’s a good idea to supply your email address so that Let’s Encrypt can send expiry notifications)
sudo docker run -it --rm \
-v /docker-volumes/etc/letsencrypt:/etc/letsencrypt \
-v /docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt \
-v /docker/letsencrypt-docker-nginx/src/letsencrypt/letsencrypt-site:/data/letsencrypt \
-v "/docker-volumes/var/log/letsencrypt:/var/log/letsencrypt" \
certbot/certbot \
certonly --webroot \
--email youremail@domain.com --agree-tos --no-eff-email \
--webroot-path=/data/letsencrypt \
-d {domain}
If everything ran successfully, run a docker-compose down command to stop the temporary Nginx site
cd /docker/letsencrypt-docker-nginx/src/letsencrypt
sudo docker-compose down
Set up Your Production Site to Run in a Nginx Docker Container
Let’s start with the docker-compose.yml file
# docker-compose.yml
version: '3.1'
services:
production-nginx-container:
container\_name: 'production-nginx-container'
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./production.conf:/etc/nginx/conf.d/default.conf
- ./production-site:/usr/share/nginx/html
- ./dh-param/dhparam-2048.pem:/etc/ssl/certs/dhparam-2048.pem
- /docker-volumes/etc/letsencrypt/live/{domain}/fullchain.pem:/etc/letsencrypt/live/{domain}/fullchain.pem
- /docker-volumes/etc/letsencrypt/live/{domain}/privkey.pem:/etc/letsencrypt/live/{domain}/privkey.pem
networks:
- docker-network
networks:
docker-network:
driver: bridge
The docker-compose does the following:
- Allows ports 80 and 443
- Maps the production Nginx configuration file into the container
- Maps the production site content into the container
- Maps a 2048 bit Diffie–Hellman key exchange file into the container
- Maps the public and private keys into the container
- Sets up a docker network
Next, create the Nginx configuration file for the production site
production.conf
# production.conf
server {
listen 80;
listen [::]:80;
server\_name {domain};
location / {
rewrite ^ https://$host$request\_uri? permanent;
}
#for certbot challenges (renewal process)
location ~ /.well-known/acme-challenge {
allow all;
root /data/letsencrypt;
}
}
#https://ohhaithere.com
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server\_name {domain};
server\_tokens off;
ssl\_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
ssl\_certificate\_key /etc/letsencrypt/live/{domain}/privkey.pem;
ssl\_buffer\_size 8k;
ssl\_dhparam /etc/ssl/certs/dhparam-2048.pem;
ssl\_protocols TLSv1.2 TLSv1.1 TLSv1;
ssl\_prefer\_server\_ciphers on;
ssl\_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
ssl\_ecdh\_curve secp384r1;
ssl\_session\_tickets off;
# OCSP stapling
ssl\_stapling on;
ssl\_stapling\_verify on;
resolver 8.8.8.8;
return 301 https://{domain}$request\_uri;
}
#https://{domain}
server {
server\_name {domain};
listen 443 ssl http2;
listen [::]:443 ssl http2;
server\_tokens off;
ssl on;
ssl\_buffer\_size 8k;
ssl\_dhparam /etc/ssl/certs/dhparam-2048.pem;
ssl\_protocols TLSv1.2 TLSv1.1 TLSv1;
ssl\_prefer\_server\_ciphers on;
ssl\_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
ssl\_ecdh\_curve secp384r1;
ssl\_session\_tickets off;
# OCSP stapling
ssl\_stapling on;
ssl\_stapling\_verify on;
resolver 8.8.8.8 8.8.4.4;
ssl\_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
ssl\_certificate\_key /etc/letsencrypt/live/{domain}/privkey.pem;
root /usr/share/nginx/html;
index index.html;
}
Generate a 2048 bit DH Param file
sudo openssl dhparam -out /docker/letsencrypt-docker-nginx/src/production/dh-param/dhparam-2048.pem 2048
Copy your site content into the mapped directory:
/docker/letsencrypt-docker-nginx/src/production/production-site/
Spin up the production site in a Docker container:
sudo docker-compose up -d
If you open up a browser and point to HTTP, you should see that the site loads correctly and will automatically redirect to HTTPS
How to Renew Let’s Encrypt SSL Certificates with Certbot and Docker
Earlier, we placed the following section in the production Nginx configuration file:
location ~ /.well-known/acme-challenge {
allow all;
root /usr/share/nginx/html;
}
The production site’s docker-compose file then maps a volume into the Nginx container that can be used for challenge requests:
production-nginx-container:
container\_name: 'production-nginx-container'
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
#other mapped volumes...
#for certbot challenges
- /docker-volumes/data/letsencrypt:/data/letsencrypt
networks:
- docker-network
This effectively allows Certbot to perform a challenge request. It’s important to note that certbot challenge requests will be performed using port 80 over HTTP, so ensure that you enable port 80 for your production site.
All that’s left to do is to set up a cron job that will execute a certbot command to renew Let’s Encrypt SSL certificates.
Finally — Set Up a Cron Job to Automatically Renew Let’s Encrypt SSL/TLS Certificates
It’s a good idea to run a daily cron job that attempts to renew Let’s Encrypt SSL certificates. It doesn’t matter how many times this command is executed as nothing will happen unless your certificate is due for renewal.
To add a crontab, run the following commands:
sudo crontab -e
Place the following at the end of the file, then close and save it.
0 0 \* \* \* docker run --rm -it --name certbot -v "/docker-volumes/etc/letsencrypt:/etc/letsencrypt" -v "/docker-volumes/var/lib/letsencrypt:/var/lib/letsencrypt" -v "/docker-volumes/data/letsencrypt:/data/letsencrypt" -v "/docker-volumes/var/log/letsencrypt:/var/log/letsencrypt" certbot/certbot renew --webroot -w /data/letsencrypt --quiet && docker kill --signal=HUP production-nginx-container
The above command will run every night at 00:00. If the certificates are due for renewal, the certificates will renew. Additionally, the Nginx configuration and renewed certificates will reload by executing the signal command at the end of the cron command.
Please share any thoughts or comments you have. Feel free to ask and correct me if I’ve made some mistakes.
Top comments (0)