Important
The project has been discontinued. Use linuxserver/docker-swag instead.
- 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
- Troubleshooting
Set up Nginx and Let’s Encrypt in less than 3 minutes using Docker Compose and a simple CLI tool.
This repository contains a Docker Compose project and a CLI configuration management tool to automatically obtain and renew free Let's Encrypt SSL/TLS certificates and set up HTTPS in Nginx for multiple domain names.
You can run Nginx and set up HTTPS (https://
) and WebSocket Secure (wss://
) with free Let's Encrypt SSL/TLS certificates for your domain names and get an A+ rating in SSL Labs SSL Server Test using letsencrypt-docker-compose.
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 SSL/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. Thus, this project was created to easily manage, install and auto-renew free SSL/TLS certificates with Docker Compose.
The project supports separate SSL/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.
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
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 |
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.
Copy your static content to html/${domain}
directory.
cp -R ./examples/html/ ./html/a.evgeniy-khyst.com
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
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
Copy your PHP scripts to html/${domain}
directory.
cp -R ./examples/php/ ./html/a.evgeniy-khyst.com
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.
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
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
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
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.
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.
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 |
Repeat the actions described in the subsection of the same name in the "Initial setup" section.
Run the CLI tool, choose Add new domains
and follow the instructions.
./cli.sh config
or
docker compose run --rm cli
For each new domain, check https://${domain}
and https://www.${domain}
if you've configured the www
subdomain.
Run the CLI tool, choose Remove existing domains
and follow the instructions.
./cli.sh config
or
docker compose run --rm cli
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 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.
./cli.sh config
or
docker compose run --rm cli
./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
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.
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 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
If the certbot
service fails to start (the container is unhealthy), check the logs: docker compose logs certbot
.
If the Certbot logs contain messages Certbot failed to authenticate some domains (authenticator: webroot)
and Timeout during connect (likely firewall problem)
,
this means that the Let's Encrypt servers can't connect to your server to pass HTTP-01 challenge.
-
Double-check your DNS and firewall configurtions.
-
Run the project in dry run mode (without actually running Certbot):
./cli.sh up --dry-run
-
Specify your domain:
domain=your.domain.com
-
Create a file that will emulate an HTTP-01 challenge:
docker compose exec certbot mkdir -p /var/www/certbot/${domain}/.well-known/acme-challenge/ docker compose exec certbot sh -c "echo $(date) > /var/www/certbot/${domain}/.well-known/acme-challenge/test-token.txt"
-
Make sure the file has been created:
docker compose exec certbot cat /var/www/certbot/${domain}/.well-known/acme-challenge/test-token.txt
-
Make sure Nginx is serving a file emulating an HTTP-01 challenge:
curl http://${domain}/.well-known/acme-challenge/test-token.txt
The output of the
curl
command should match the contents of thetest-token.txt
file.