letsencrypt-docker-compose
- Overview
- Initial setup
- Installing
- Step 1 - Create DNS records
- Step 2 - Configure Nginx
- Step 3 - Perform an initial setup using the CLI tool
- Step 4 - Start the services
- Step 5 - Verify that HTTPS works with the test certificates
- Step 6 - Switch to a Let's Encrypt production environment
- Step 7 - Verify that HTTPS works with the production certificates
- Updating
- Adding new domains without downtime
- Removing existing domains without downtime
- Manually renewing all Let's Encrypt certificates
- Running on a local machine not directed to by DNS records
- Advanced Nginx configuration
- Running Docker containers as a non-root user
- SSL configuration for A+ rating
Overview
Set up Nginx and Let’s Encrypt with Docker Compose in less than 3 minutes.
This repository contains a Docker Compose project, which automatically obtains and renews free Let's Encrypt SSL/TLS certificates and sets up HTTPS in Nginx for multiple domain names and a simple CLI configuration management tool.
You can run Nginx and set up HTTPS (https://
) and WebSocket Secure (wss://
) with Let's Encrypt TLS certificates for your domain names and get an A+ rating in SSL Labs SSL Server Test using Docker Compose and letsencrypt-docker-compose interactive CLI tool.
Nginx is configured to support IPv4, IPv6, HTTP/1.1, HTTP/2, and optionally, WebSocket.
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 cloud native certificate management solutions. 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 project supports separate TLS certificates for multiple domain names.
The idea is simple. There are three main services:
nginx
,certbot
for obtaining and renewing certificates,cron
for triggering certificates renewal,
and one additional service cli
for interactive configuration.
The sequence of actions:
- You perform an initial setup with letsencrypt-docker-compose CLI tool.
- 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.
Initial setup
Installing
Make sure you have up to date versions of Docker and Docker Compose installed.
Clone this repository (or create and clone a fork):
git clone https://github.com/evgeniy-khist/letsencrypt-docker-compose.git
Step 1 - Create DNS records
You need to have a domain name and a server with a publicly routable IP address.
For simplicity, this example deals with domain names a.evgeniy-khyst.com
and b.evgeniy-khyst.com
,
but in reality, domain names can be any (e.g., example.com
, anotherdomain.net
).
For all domain names create DNS A or AAAA record, or both to point to a server where Docker containers will be running.
Also, create CNAME records for the www
subdomains if needed.
DNS records
Type | Hostname | Value |
---|---|---|
A | a.evgeniy-khyst.com |
directs to IPv4 address |
A | b.evgeniy-khyst.com |
directs to IPv4 address |
AAAA | a.evgeniy-khyst.com |
directs to IPv6 address |
AAAA | b.evgeniy-khyst.com |
directs to IPv6 address |
CNAME | www.a.evgeniy-khyst.com |
is an alias of a.evgeniy-khyst.com |
CNAME | www.a.evgeniy-khyst.com |
is an alias of a.evgeniy-khyst.com |
Step 2 - Configure Nginx
Nginx can be configured
- to serve static content,
- as a reverse proxy (e.g., proxying all requests to a backend server),
- to proxy requests to PHP-FPM.
Serving static content
Copy your static content to html/${domain}
directory.
cp -R ./examples/html/ ./html/a.evgeniy-khyst.com
Reverse proxy
Single Docker Compose project
The docker-compose.yml
contains the example-backend
service.
It's a simple Node.js web app listening on port 8080.
It has /hello?name={name}
REST endpoint and WebSocket echo server sending back the request sent by the client.
Replace it with your backend service or remove it.
services:
example-backend:
build: ./examples/nodejs-backend
image: evgeniy-khyst/expressjs-helloworld
restart: unless-stopped
Multiple Docker Compose projects
If your upstream server is defined in the YAML file of another Docker Compose project,
configure it to join the letsencrypt-docker-compose_default
network created by this project,
so Nginx is able to forward requests to the upstream service.
Define a reference to the letsencrypt-docker-compose_default
network in your other YAML file.
version: "3"
services:
example-backend:
build: ./examples/nodejs-backend
image: evgeniy-khyst/expressjs-helloworld
networks:
- letsencrypt-docker-compose
restart: unless-stopped
networks:
letsencrypt-docker-compose:
name: letsencrypt-docker-compose_default
external: true
PHP-FPM
Copy your PHP scripts to html/${domain}
directory.
cp -R ./examples/php/ ./html/a.evgeniy-khyst.com
Step 3 - Perform an initial setup using the CLI tool
Run the CLI tool and follow the instructions to perform an initial setup.
./cli.sh config
or
docker compose run --rm cli
On the first run, choose to obtain a test certificate from a Let's Encrypt staging server. We will switch to a Let's Encrypt production environment after verifying that HTTPS is working with the test certificate.
Step 4 - Start the services
Start the services.
./cli.sh up
or
docker compose up -d
All Docker images used in the project are multi-platform and support amd64
, arm32v6
, and arm64v8
architectures.
For example, when running the project on an x86_64
/amd64
machine, the amd64
variants are pulled and run.
Check the logs.
docker compose logs -f
For each domain wait for the following log messages:
Switching Nginx to use Let's Encrypt certificate
Reloading Nginx configuration
Step 5 - Verify that HTTPS works with the test certificates
For each domain, check https://${domain}
and https://www.${domain}
if you've configured the www
subdomain.
Certificates issued by (STAGING) Let's Encrypt
are considered not secure by browsers and cURL.
curl --insecure https://a.evgeniy-khyst.com
curl --insecure https://www.a.evgeniy-khyst.com
curl --insecure https://b.evgeniy-khyst.com/hello?name=Eugene
curl --insecure https://www.b.evgeniy-khyst.com/hello?name=Eugene
If you've set up WebSocket, check it using the wscat tool.
wscat --no-check --connect wss://b.evgeniy-khyst.com/echo
Step 6 - Switch to a Let's Encrypt production environment
Run the CLI tool, choose Switch to a Let's Encrypt production environment
and follow the instructions.
./cli.sh config
or
docker compose run --rm cli
Step 7 - Verify that HTTPS works with the production certificates
For each domain, check https://${domain}
and https://www.${domain}
if you've configured the www
subdomain.
Certificates issued by Let's Encrypt
are considered secure by browsers and cURL.
curl https://a.evgeniy-khyst.com
curl https://www.a.evgeniy-khyst.com
curl https://b.evgeniy-khyst.com/hello?name=Eugene
curl https://www.b.evgeniy-khyst.com/hello?name=Eugene
If you've set up WebSocket, check it using the wscat tool.
wscat --connect wss://b.evgeniy-khyst.com/echo
Optionally check your domains with SSL Labs SSL Server Test and review the SSL Reports.
The cron
service will automatically renew the Let's Encrypt production certificates when the time comes.
Updating
If you haven't forked the repo, just pull the latest changes.
git pull
If you've forked the repo, sync with upstream.
git remote add upstream https://github.com/evgeniy-khist/letsencrypt-docker-compose.git
git fetch upstream
git checkout main
git rebase upstream/main
After getting the latest changes, rebuild the Docker images of the CLI tool and all services.
./cli.sh build
or
docker compose --profile config build
Also, you need to rebuild the Docker images of the services if you've made any changes to them.
Adding new domains without downtime
Step 1 - Create new DNS records
Create DNS A or AAAA record, or both.
Also, create CNAME record for www
subdomain if needed.
DNS records
Type | Hostname | Value |
---|---|---|
A | c.evgeniy-khyst.com |
directs to IPv4 address |
AAAA | c.evgeniy-khyst.com |
directs to IPv6 address |
CNAME | www.c.evgeniy-khyst.com |
is an alias of c.evgeniy-khyst.com |
Step 2 - Copy static content or define upstream service
Repeat the actions described in the subsection of the same name in the "Initial setup" section.
Step 3 - Update the configuration using the CLI tool
Run the CLI tool, choose Add new domains
and follow the instructions.
./cli.sh config
or
docker compose run --rm cli
Step 4 - Verify that HTTPS works
For each new domain, check https://${domain}
and https://www.${domain}
if you've configured the www
subdomain.
Removing existing domains without downtime
Run the CLI tool, choose Remove existing domains
and follow the instructions.
./cli.sh config
or
docker compose run --rm cli
Manually renewing all Let's Encrypt certificates
You can manually renew all of your certificates.
Certbot renewal will be executed with --force-renewal
flag that causes the expiration time of the certificates to be ignored when considering renewal, and attempts to renew each and every installed certificate regardless of its age.
This operation is not appropriate to run daily because each certificate will be renewed every day, which will quickly run into the Let's Encrypt rate limit.
Run the CLI tool, choose Manually renew all Let's Encrypt certificates (force renewal)
and follow the instructions.
./cli.sh config
or
docker compose run --rm cli
Running on a local machine not directed to by DNS records
Running Certbot on a local machine not directed to by DNS records makes no sense because Let’s Encrypt servers will fail to validate that you control the domain names in the certificate.
But it may be useful to run all services locally with disabled Certbot. It is possible in dry run mode.
Step 1 - Perform an initial setup using the CLI tool
./cli.sh config
or
docker compose run --rm cli
Step 2 - Start the services in dry run mode
./cli.sh up --dry-run
Alternatively, you can enable dry run mode using the environment variable DRY_RUN=true
.
DRY_RUN=true docker compose up -d
Advanced Nginx configuration
You can configure Nginx by manually editing the nginx-conf/nginx.conf
.
Configure virtual hosts (server
blocks) by editing the nginx-conf/conf.d/${domain}.conf
.
Any .conf
file from the nginx-conf/conf.d
directory is included in the Nginx configuration.
For example, to declare upstream servers, edit nginx-conf/conf.d/upstreams.conf
upstream backend {
server backend1.example.com:8080;
server backend2.example.com:8080;
}
After editing the Nginx configuration, do a hot reload of the Nginx configuration.
Run the CLI tool and choose Reload Nginx configuration without downtime
.
./cli.sh config
or
docker compose run --rm cli
Manual edits of the nginx-conf/nginx.conf
and nginx-conf/conf.d/${domain}.conf
are lost after running the CLI tool
(e.g., adding or removing domains or switching to a Let's Encrypt production environment).
The CLI tool generates the Nginx configuration files based on the config.json
.
To make Nginx configuration changes persistent, also edit the Handlebars templates used for their generation
To add domain-specific configuration to a template use the ifEquals
Handlebars helper.
Running Docker containers as a non-root user
By default, Docker is only accessible with root privileges (sudo
).
The CLI tool creates the following files in the hosts' project root directory mounted into the container:
config.json
,nginx-conf/nginx.conf
,nginx-conf/conf.d/${domain}.conf
.
These files will be owned by the root
user.
When non-root users try to clean up or edit these files, they get the "permission denied" error.
If you want to use Docker as a regular user, you need to add your user to the docker
group.
To make the CLI tool create files in a way that allows non-root users to edit and delete them,
tell Docker Compose to run as the current user instead of root
.
As the CLI tool runs Docker Compose commands internally, specify the docker
group as a supplementary group,
so the user inside the container will be its member.
We have to use user IDs and group IDs because containers don't know their associated usernames and group names.
Run the CLI tool specifying the current user and docker
group to make it create files owned by the current user.
CURRENT_USER="$(id -u):$(id -g)" DOCKER_GROUP="$(getent group docker | cut -d: -f3)" docker compose run --rm cli
The convenience script cli.sh
runs the CLI tool as the current user by default.
./cli.sh config
You can run the CLI tool as UID/GID 0 instead of the current user with the option --no-current-user
.
./cli.sh config --no-current-user
SSL configuration for A+ rating
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: