- Overview
- Initial setup
- Prerequisites
- Step 0 - Create DNS records
- Step 1 - Edit domain names and emails in the configuration
- Step 2 - Configure Nginx virtual hosts
- Step 3 - Create named Docker volumes for dummy and Let's Encrypt TLS certificates
- Step 4 - Build images and start containers using staging Let's Encrypt server
- Step 5 - verify HTTPS works with the staging certificates
- Step 6 - Switch to production Let's Encrypt server
- Step 7 - verify HTTPS works with the production certificates
- Reloading Nginx configuration without downtime
- Adding a new domain to a running solution
- Directory structure
- Configuration file structure
- SSL configuration for A+ rating
- Removing a domain name from a running solution
This example automatically obtains and renews Let's Encrypt TLS certificates and sets up HTTPS in Nginx for multiple domain names using Docker Compose.
You can set up HTTPS in Nginx with Let's Encrypt TLS certificates for your domain names and get an A+ rating in SSL Labs SSL Server Test by changing a few configuration parameters of this example.
Let's Encrypt is a certificate authority that provides free X.509 certificates for TLS encryption. The certificates are valid for 90 days and can be renewed. Both initial creation and renewal can be automated using Certbot.
When using Kubernetes Let's Encrypt TLS certificates can be easily obtained and installed using Cert Manager. For simple websites and applications, Kubernetes is too much overhead and Docker Compose is more suitable. But for Docker Compose there is no such popular and robust tool for TLS certificate management.
The example supports separate TLS certificates for multiple domain names, e.g. example.com
, anotherdomain.net
etc.
For simplicity this example deals with the following domain names:
test1.evgeniy-khyst.com
test2.evgeniy-khyst.com
The idea is simple. There are 3 containers:
- Nginx
- Certbot - for obtaining and renewing certificates
- Cron - for triggering certificates renewal once a day
The sequence of actions:
- Nginx generates self-signed "dummy" certificates to pass ACME challenge for obtaining Let's Encrypt certificates
- Certbot waits for Nginx to become ready and obtains certificates
- Cron triggers Certbot to try to renew certificates and Nginx to reload configuration daily
- Docker and Docker Compose are installed
- You have a domain name
- You have a server with a publicly routable IP address
- You have cloned this repository (or created and cloned a fork):
git clone https://github.com/evgeniy-khist/letsencrypt-docker-compose.git
For all domain names create DNS A records to point to a server where Docker containers will be running.
Also, consider creating CNAME records for the www
subdomains.
DNS records
Type | Hostname | Value |
---|---|---|
A | test1.evgeniy-khyst.com |
directs to IP address X.X.X.X |
A | test2.evgeniy-khyst.com |
directs to IP address X.X.X.X |
CNAME | www.test1.evgeniy-khyst.com |
is an alias of test1.evgeniy-khyst.com |
CNAME | www.test2.evgeniy-khyst.com |
is an alias of test2.evgeniy-khyst.com |
Specify your domain names and contact emails for these domains with space as delimiter in the config.env
:
DOMAINS="test1.evgeniy-khyst.com test2.evgeniy-khyst.com"
CERTBOT_EMAILS="info@evgeniy-khyst.com info@evgeniy-khyst.com"
For two and more domains separated by space use double quotes ("
) around the DOMAINS
and CERTBOT_EMAILS
variables.
For a single domain double quotes can be omitted:
DOMAINS=test1.evgeniy-khyst.com
CERTBOT_EMAILS=info@evgeniy-khyst.com
For each domain configure the Nginx server
block by updating vhosts/${domain}.conf
:
vhosts/test1.evgeniy-khyst.com.conf
vhosts/test2.evgeniy-khyst.com.conf
location / {
root /var/www/html/my-domain;
index index.html index.htm;
}
Make sure html/my-domain
directory (relative to the repository root) exists and countains the desired content and html
directory is mounted as /var/www/html
in docker-compose.yml
:
services:
nginx:
#...
volumes:
#...
- ./html:/var/www/html
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://my-backend:8080/;
}
my-backend
is the service name of your backend application in docker-compose.yml
:
services:
my-backend:
image: example.com/my-backend:1.0.0
#...
ports:
- "8080"
docker volume create --name=nginx_conf
docker volume create --name=letsencrypt_certs
docker compose up -d --build
docker compose logs -f
You can alternatively use the docker-compose
binary.
For each domain wait for the following log messages:
Switching Nginx to use Let's Encrypt certificate
Reloading Nginx configuration
For each domain open in browser https://${domain}
and https://www.${domain}
and verify that staging Let's Encrypt certificates are working:
- https://test1.evgeniy-khyst.com, https://www.test1.evgeniy-khyst.com
- https://test2.evgeniy-khyst.com, https://www.test2.evgeniy-khyst.com
Certificates issued by (STAGING) Let's Encrypt
are considered not secure by browsers.
Stop the containers:
docker compose down
Configure to use production Let's Encrypt server in config.env
:
CERTBOT_TEST_CERT=0
Re-create the volume for Let's Encrypt certificates:
docker volume rm letsencrypt_certs
docker volume create --name=letsencrypt_certs
Start the containers:
docker compose up -d
docker compose logs -f
For each domain open in browser https://${domain}
and https://www.${domain}
and verify that production Let's Encrypt certificates are working.
Certificates issued by Let's Encrypt
are considered secure by browsers.
Optionally check your domains with SSL Labs SSL Server Test and review the SSL Reports.
Update a configuration in vhosts/${domain}.conf
.
Do a hot reload of the Nginx configuration:
docker compose exec --no-TTY nginx nginx -s reload
Let's add a third domain test3.evgeniy-khyst.com
to a running solution.
Create DNS A record and CNAME record for www
subdomain.
DNS records
Type | Hostname | Value |
---|---|---|
A | test3.evgeniy-khyst.com |
directs to IP address X.X.X.X |
CNAME | www.test3.evgeniy-khyst.com |
is an alias of test3.evgeniy-khyst.com |
Add a new domain name (test3.evgeniy-khyst.com
) and contact email to the config.env
:
DOMAINS="test1.evgeniy-khyst.com test2.evgeniy-khyst.com test3.evgeniy-khyst.com"
CERTBOT_EMAILS="info@evgeniy-khyst.com info@evgeniy-khyst.com info@evgeniy-khyst.com"
Create a virtual host configuration file vhosts/test3.evgeniy-khyst.com.conf
for the new domain.
For example, for serving static content use the following configuration:
location / {
root /var/www/html/test3.evgeniy-khyst.com;
index index.html index.htm;
}
Create a webroot html/test3.evgeniy-khyst.com
and add static content.
docker compose down
docker compose up -d
docker compose logs -f
docker-compose.yml
.env
- specifiesCOMPOSE_PROJECT_NAME
to make container names independent from the base directory nameconfig.env
- specifies project configuration, e.g. domain names, emails etc.nginx/
Dockerfile
nginx.sh
- entrypoint scriptdefault.conf
- common settings for all domains. The file is copied to/etc/nginx/conf.d/
gzip.conf
- Gzip compression. Included indefault.conf
site.conf.tpl
- virtual host configuration template used to create configuration files/etc/nginx/sites/${domain}.conf
included indefault.conf
options-ssl-nginx.conf
- a configuration to get A+ rating at SSL Server Test. Included insite.conf.tpl
hsts.conf
- HTTP Strict Transport Security (HSTS) policy. Included insite.conf.tpl
vhosts/
test1.evgeniy-khyst.com.conf
-server
block configuration for serving static content. Included insite.conf.tpl
(include /etc/nginx/vhosts/${domain}.conf;
)test2.evgeniy-khyst.com.conf
-server
block configuration for serving static content. Included insite.conf.tpl
(include /etc/nginx/vhosts/${domain}.conf;
)
html/
test1.evgeniy-khyst.com/
- directory mounted as a webroot fortest1.evgeniy-khyst.com
. Configured invhosts/test1.evgeniy-khyst.com.conf
test2.evgeniy-khyst.com/
- directory mounted as a webroot fortest2.evgeniy-khyst.com
. Configured invhosts/test2.evgeniy-khyst.com.conf
certbot/
Dockerfile
certbot.sh
- entrypoint script
cron/
Dockerfile
renew_certs.sh
- script executed on a daily basis to try to renew certificates
To adapt the example to your domain names you need to change only config.env
:
DOMAINS="test1.evgeniy-khyst.com test2.evgeniy-khyst.com"
CERTBOT_EMAILS="info@evgeniy-khyst.com info@evgeniy-khyst.com"
CERTBOT_TEST_CERT=1
CERTBOT_RSA_KEY_SIZE=4096
Configuration parameters:
DOMAINS
- a space separated list of domains to manage certificates forCERTBOT_EMAILS
- a space separated list of email for corresponding domains. If not specified, certificates will be obtained with--register-unsafely-without-email
CERTBOT_TEST_CERT
- use Let's Encrypt staging server (--test-cert
)
Let's Encrypt has rate limits. So, while testing it's better to use staging server by setting CERTBOT_TEST_CERT=1
(default value).
When you are ready to use production Let's Encrypt server, set CERTBOT_TEST_CERT=0
.
SSL in Nginx is configured accoring to best practices to get A+ rating in SSL Labs SSL Server Test.
Read more about the best practices and rating:
- https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
- https://github.com/ssllabs/research/wiki/SSL-Server-Rating-Guide
Remove the domain.to.remove.com.conf
file from vhost
Remove the domain name from config.env
docker compose down
docker volume rm nginx_conf
docker volume create --name=nginx_conf
docker compose up -d