Self-hosted service
nginx-le/nginx-le avatar
nginx-le/nginx-le

nginx-le: an Alpine nginx image that keeps its own certificate alive

Nginx with automatic let's encrypt (docker image)

686 stars119 forksShellApache-2.0

At a glance

What is it?
nginx-le is a small Docker image built on nginx:1.30-alpine that requests a Let's Encrypt certificate on boot and refreshes it when it nears expiry. It suits people who want a working TLS reverse proxy quickly, and it is not a fit if you need a full ingress controller or full control of the certificate lifecycle.
Who is it for?
Pick nginx-le when you need a TLS terminating reverse proxy for a handful of services and would rather not run certbot by hand: the image handles issuance, renewal and the http to https redirect, and the compose file gives you a working example. Skip it if you need a dynamic routing layer, if you want to bring a certificate from a CA that still publishes OCSP responders, or if the ten-day refresh cycle and single-certbot-per-domain model do not match your certificate plan.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A plain nginx image with certbot welded on

The problem is small and familiar. You want nginx in front of a service, you want HTTPS, and you do not want to manage a certbot timer on the host and hope the renewal cron fires at three in the morning. nginx-le answers that in one Alpine based image. The Dockerfile starts from `nginx:1.30-alpine`, adds the project `conf/nginx.conf`, copies in `script/entrypoint.sh` and `script/le.sh`, installs Alpine's `certbot` package along with `tzdata` and `openssl`, and runs the entrypoint as its command.

What you get back is a normal nginx with a certificate lifecycle attached. The project describes itself as a simple nginx image with integrated Let's Encrypt support, and the topic tags on the repository cover automation, docker, letsencrypt, nginx, proxy and ssl-certificates. The image is published on Docker Hub as `umputun/nginx-le`, and the compose file in the repository builds locally and names the image the same way.

The scope is deliberately narrow. This is not a control plane, and it does not try to be. It is an image for people who already know nginx and want the certificate part handled. The homepage is empty, the primary language is Shell, and the licence is Apache-2.0.

Your config file gets placeholders filled at container start

The mechanism that ties the container together is a text substitution pass over the custom configuration you mount. On start, the container replaces matching text in your mounted files with values taken from the environment. The documented patterns are `SSL_CERT`, which becomes `/etc/nginx/ssl/$SSL_CERT` for the `ssl_certificate` directive, `SSL_KEY`, which becomes `/etc/nginx/ssl/$SSL_KEY` for `ssl_certificate_key`, `SSL_CHAIN_CERT`, which becomes `/etc/nginx/ssl/$SSL_CHAIN_CERT` for `ssl_trusted_certificate`, and `LE_FQDN`, which expands to the domain list with commas turned into spaces inside `server_name`.

That last one is the detail worth understanding. A single `server` block can serve several domains, and the FQDN list is written with commas in the environment but must appear space separated in `server_name`. The image handles the conversion so you do not have to maintain two spellings of the same list.

Because the substitution is plain text replacement, the ssl directives in your own `etc/service.conf` have to be written in the form the image expects, which is why the setup notes ask you to copy `etc/service-example.conf` and keep these lines as they are:

bash
ssl_certificate SSL_CERT;
ssl_certificate_key SSL_KEY;
ssl_trusted_certificate SSL_CHAIN_CERT;

There is a second escape hatch for anything the table does not cover. Mount your configuration as `/etc/nginx/templates/yourservice.conf.template` and `envsubsts` runs on that file, which is the standard nginx image mechanism for substituting arbitrary environment variables.

A first run you can smoke test without a domain

The fastest way to see whether the image behaves is to skip Let's Encrypt entirely and ask for the built-in redirect. The documentation gives a local smoke test that builds the image, starts it with `LETSENCRYPT=false`, and publishes port 8080 on the loopback interface:

shell
docker build -t nginx-le .
docker run -d --name nginx-le-smoke -e LETSENCRYPT=false -p 127.0.0.1:8080:80 nginx-le

# the first start generates dh parameters and takes a while
until curl -s -o /dev/null http://127.0.0.1:8080/; do sleep 2; done
curl -sI http://127.0.0.1:8080/ | head -1   # HTTP/1.1 301 Moved Permanently

docker rm -f nginx-le-smoke

The expectation is a `HTTP/1.1 301 Moved Permanently` response, which proves the redirect from port 80 to https is in place. The loop is there because the first start generates DH parameters and takes a while, so the script waits rather than assuming a fixed delay.

For the real thing, the flow is to take `docker-compose.yml`, edit it, and either pull the published image or build it yourself. The commands named in the documentation are `docker-compose pull` if you do not want to build, `docker-compose build` if you do, and `docker-compose up` to start. The compose file in the repository is the reference for what the edited file should look like:

yaml
        volumes:
            - ./etc/ssl:/etc/nginx/ssl
            - ./etc/service-example.conf:/etc/nginx/service.conf
        ports:
            - "80:80"
            - "443:443"
        environment:
            - TZ=America/Chicago
            - LETSENCRYPT=true
            - [email protected]
            - LE_FQDN=www.example.com

The `LETSENCRYPT=true` switch, `LE_EMAIL` and `LE_FQDN` are the three settings that matter. A comma separated list works for several domains, so `LE_FQDN=aaa.example.com,bbb.example.com` covers a site with more than one name. `TZ` sets the container timezone, and the notes point at `/usr/share/zoneinfo` for valid values.

The entrypoint renews on a ten-day cycle and four separate checks

The renewal logic lives in `script/entrypoint.sh`, which requests the certificate at start and then refreshes it every 10 days when a set of conditions is met. The conditions are worth reading closely, because they define when the image bothers to talk to the certificate authority again.

A refresh is triggered when the certificate is close to expiration at 30 days, when it misses one of the `LE_FQDN` domains, when it does not match the installed key, or when it has lost its chain file. In other words, the image checks the installed state and repairs it, rather than renewing on a fixed schedule regardless of what is already on disk. The actual certificate retrieval is in `script/le.sh`.

Two implementation details are called out in the notes. The image uses Alpine's `certbot` package rather than a pip install, which keeps the image small and avoids a Python dependency in the runtime. And OCSP stapling is not enabled, because Let's Encrypt retired its OCSP responders and its certificates carry no responder URL. The documentation does add the caveat that if you bring your own certificate from a CA that still publishes one, or one with the Must-Staple extension, you should turn on `ssl_stapling` and `ssl_stapling_verify` in your own `server` block.

The image also reports an A+ overall rating on ssllabs, which is a third-party measurement of the default configuration rather than a promise about your own edited files.

One config, many services, and the stream section for raw TCP

Serving more than one upstream does not require a second container. The compose file mounts your own configuration by filename, and the naming convention is the contract. Files matching `service*.conf` are copied into the container, and `service2.conf` in the compose file is the reference for the pattern. The alternative is to mount a whole directory of `*.conf` files into `/etc/nginx/conf.d-le` and have them all copied at once.

Raw TCP works the same way through the stream module. Files matching `stream*.conf` are picked up into `/etc/nginx/stream.d/` and included into the `stream` section of the nginx configuration, with `stream2.conf` in the compose file given as the reference. The directory variant is `/etc/nginx/stream.conf.d-le`.

One behaviour is worth internalising before you write a config. The provided `nginx.conf` handles the http to https redirect automatically, so you do not add it to your `service.conf`. The exception is a custom server on port 80, and the documentation gives the exact fragment needed so the challenge path still resolves:

bash
location /.well-known/ {root /usr/share/nginx/html;}

That root setting matters because the Let's Encrypt challenge is served over plain http before a certificate exists. A custom port 80 server without it can break issuance in a way that is hard to read from the container logs.

When nginx-le is the wrong tool

The project lists its own alternatives, and reading them tells you the boundary of the design. Traefik is an HTTP reverse proxy and load balancer that supports Let's Encrypt directly. Caddy also supports Let's Encrypt directly. leproxy is a small stand-alone https reverse proxy with automatic Let's Encrypt, and Reproxy is an edge HTTP(s) server and reverse proxy supporting various providers.

The difference in approach is the point. Those tools generate and manage certificates as part of a routing product, where a host rule triggers a certificate request. nginx-le keeps the two things separate: nginx configuration stays in your own files, and the certificate is a container concern handled by the entrypoint. If you want a single declarative file that both routes and certifies, Caddy or Traefik will get you there with less glue. If you already have nginx configs and just want TLS handled, that is exactly what this image does.

There are limits to note as well. Renewal is per container and driven by the entrypoint, so running a scaled replica set means several certbot clients, which is a situation the documentation does not describe. The compose example maps both `80:80` and `443:443` on the host, which assumes a dedicated host or a port that is free. And the maintenance picture is good but young: the last push was on 2026-10-11, the same day v1.8.0 was tagged, with v1.7.1 on 2026-05-14 and v1.7.0 on 2024-09-07 before it, so the image is moving but the release history is short.

Editorial conclusion

Pick nginx-le when you need a TLS terminating reverse proxy for a handful of services and would rather not run certbot by hand: the image handles issuance, renewal and the http to https redirect, and the compose file gives you a working example. Skip it if you need a dynamic routing layer, if you want to bring a certificate from a CA that still publishes OCSP responders, or if the ten-day refresh cycle and single-certbot-per-domain model do not match your certificate plan. First verify that `LE_FQDN` resolves to a host already pointing at the container, that port 80 stays reachable for the challenge, and that your `etc/service.conf` keeps the three ssl directives unchanged.

Frequently asked questions

How do I enable automatic Let's Encrypt certificates in nginx-le?

Set `LETSENCRYPT=true` together with `LE_EMAIL` and `LE_FQDN` in the compose environment. For several domains pass a comma separated list such as `LE_FQDN=aaa.example.com,bbb.example.com`, and the image converts the commas to spaces for `server_name`.

Can I use my own SSL certificate with nginx-le instead of Let's Encrypt?

Yes. Set `LETSENCRYPT` to `false` and pass your own certificate in `SSL_CERT`, the key in `SSL_KEY` and the chain in `SSL_CHAIN_CERT`. The defaults are `le-crt.pem`, `le-key.pem` and `le-chain-crt.pem`, and the files are read from `/etc/nginx/ssl/`.

How often does nginx-le renew the certificate?

The entrypoint refreshes it every 10 days when the certificate is close to expiration at 30 days, when it misses one of the `LE_FQDN` domains, when it does not match the installed key, or when it has lost its chain file.

How do I serve several services from one nginx-le container?

Mount your files using the `service*.conf` naming pattern, as in `service2.conf` in the compose file, or mount a directory of `*.conf` files into `/etc/nginx/conf.d-le`. For raw TCP, `stream*.conf` files are picked up into `/etc/nginx/stream.d/`, or you can mount a directory into `/etc/nginx/stream.conf.d-le`.

Does nginx-le enable OCSP stapling?

No. OCSP stapling is not enabled because Let's Encrypt retired its OCSP responders and its certificates carry no responder URL. If you bring a certificate from a CA that still publishes a responder URL, or one with the Must-Staple extension, enable `ssl_stapling` and `ssl_stapling_verify` in your own `server` block.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. nginx-le/nginx-le on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nginx-le-nginx-le.svg)](https://hysenlabs.com/projects/nginx-le-nginx-le)

Was this useful?