/letsencrypt-docker-compose

Set up Nginx and Let’s Encrypt in less than 3 minutes with a Docker Compose project that automatically obtains and renews free Let's Encrypt SSL/TLS certificates and sets up HTTPS in Nginx for multiple domain names. Configuration is done using a simple CLI tool.

Primary LanguageJavaScriptApache License 2.0Apache-2.0

letsencrypt-docker-compose

The project has been discontinued. Use linuxserver/docker-swag instead.

Overview

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:

  1. You perform an initial setup with letsencrypt-docker-compose CLI tool.
  2. Nginx generates self-signed "dummy" certificates to pass ACME challenge for obtaining Let's Encrypt certificates.
  3. Certbot waits for Nginx to become ready and obtains certificates.
  4. Cron triggers Certbot to try to renew certificates and Nginx to reload configuration daily.

Back to top

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.

letsencrypt-docker-compose CLI initial setup

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

letsencrypt-docker-compose CLI switch to a Let's Encrypt production environment

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.

Back to top

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.

Back to top

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.

Back to top

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

Back to top

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

Back to top

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

Back to top

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.

{{#ifEquals domain "a.evgeniy-khyst.com"}}
  # Configuration for a specific domain
{{/ifEquals}}

Back to top

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

Back to top

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:

Back to top

Troubleshooting

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.

  1. Double-check your DNS and firewall configurtions.

  2. Run the project in dry run mode (without actually running Certbot):

    ./cli.sh up --dry-run
  3. Specify your domain:

    domain=your.domain.com
  4. 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"
  5. Make sure the file has been created:

    docker compose exec certbot cat /var/www/certbot/${domain}/.well-known/acme-challenge/test-token.txt
  6. 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 the test-token.txt file.

Back to top