Site icon GetPageSpeed

NGINX ACME Module: Let’s Encrypt SSL Without Certbot

NGINX ACME Module: Automatic Let's Encrypt SSL Without Certbot

Managing SSL certificates with Certbot works, but it bolts a second moving part onto your web server. You install a separate client, schedule a renewal cron job or systemd timer, add a DNS plugin and its Python dependencies, and wire up a deploy hook so NGINX actually picks up the renewed certificate. Every one of those parts can drift, fail silently, and let a certificate expire at the worst possible moment. The nginx acme module removes that entire layer.

With nginx-module-acme, NGINX itself speaks the ACME protocol to Let’s Encrypt, completes the challenge, obtains the certificate, keeps it in shared memory, and renews it automatically before it expires. There is no Certbot, no cron job, no Python, and no reload dance. You declare an issuer and a certificate in nginx.conf, and your server provisions its own TLS.

This guide shows you how to install and configure the nginx acme module on RHEL-based and Debian-based systems, how it works under the hood, and when you still need Certbot.

What the NGINX ACME Module Does

nginx-module-acme is the official ACME client from the NGINX team, packaged by GetPageSpeed as a dynamic module. It implements ACMEv2, the same protocol Certbot uses, but it runs entirely inside the NGINX worker process. Instead of writing certificate files to disk for NGINX to read, the module hands the certificate to NGINX directly through two run-time variables.

In practice, this means:

The module supports the http-01 and tls-alpn-01 challenge types. It does not support dns-01, which matters for wildcard certificates. We cover that limitation in the Certbot comparison below.

How It Works

When NGINX starts, the module reads your acme_issuer block and registers an ACME account with the certificate authority. For each server that references an issuer through acme_certificate, the module places an order, answers the challenge, and stores the resulting certificate.

For the default http-01 challenge, the module automatically serves the validation token under /.well-known/acme-challenge/. You do not need to add a location block for it. The certificate authority connects to your domain on port 80, the module answers, and validation completes.

The issued certificate and its private key live in a shared memory zone (declared with acme_shared_zone) and are persisted to a state directory on disk. NGINX then serves that certificate through the $acme_certificate and $acme_certificate_key variables, which you assign to ssl_certificate and ssl_certificate_key. Because the certificate is delivered through variables rather than static file paths, the module can swap in a renewed certificate transparently, with zero downtime.

Installation

The nginx acme module ships from the GetPageSpeed Premium Repository as a ready-to-use dynamic module. You do not compile anything.

RHEL, CentOS, AlmaLinux, Rocky Linux

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-acme

Then load the module at the very top of /etc/nginx/nginx.conf, in the main context:

load_module modules/ngx_http_acme_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-acme

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

You can browse the module package pages here: RPM package page and APT package page.

Configuration

A complete, working configuration has three parts: a shared memory zone, an issuer, and one or more servers that reference the issuer. Here is a production-ready example for Let’s Encrypt.

# /etc/nginx/nginx.conf — main context
load_module modules/ngx_http_acme_module.so;

# ... user, worker_processes, events { } ...

http {
    # The module needs a resolver to reach the ACME server.
    resolver 1.1.1.1 8.8.8.8 ipv6=off;

    # Shared memory for issued certificates and ACME state.
    acme_shared_zone zone=ngx_acme_shared:1M;

    acme_issuer letsencrypt {
        uri         https://acme-v02.api.letsencrypt.org/directory;
        contact     mailto:admin@example.com;
        state_path  /var/lib/nginx/acme-letsencrypt;
        accept_terms_of_service;
    }

    # Port 80 serves the http-01 challenge automatically.
    server {
        listen      80;
        listen      [::]:80;
        server_name example.com www.example.com;

        acme_certificate letsencrypt;

        location / {
            root /usr/share/nginx/html;
        }
    }

    # Port 443 serves the auto-issued certificate.
    server {
        listen      443 ssl;
        listen      [::]:443 ssl;
        http2       on;
        server_name example.com www.example.com;

        acme_certificate    letsencrypt;
        ssl_certificate     $acme_certificate;
        ssl_certificate_key $acme_certificate_key;

        location / {
            root /usr/share/nginx/html;
        }
    }
}

The [::]:80 and [::]:443 listeners matter: some resolvers hand back an IPv6 address for validation, so the challenge port must answer over IPv6 as well as IPv4.

Directive Reference

The module exposes the following directives. Each is shown with its valid configuration context.

acme_shared_zone zone=name:size — context: http. Declares the shared memory zone used for certificate storage and ACME state. The default is zone=ngx_acme_shared:256k. Increase it if you manage many certificates.

acme_issuer name { ... } — context: http. Defines a certificate authority and the account used to talk to it. You can declare more than one issuer (for example, a staging and a production Let’s Encrypt). The following directives are valid inside an acme_issuer block:

acme_certificate issuer [identifier ...] [key=alg[:size]] — context: server. Tells a server to provision a certificate from the named issuer. By default, the identifiers come from server_name. You can pass explicit identifiers or set the key type, for example acme_certificate letsencrypt example.com key=ecdsa:384.

The module also provides two embedded variables, valid in the http context:

Testing

Before you point the module at production Let’s Encrypt, test against the staging endpoint. Production Let’s Encrypt enforces strict rate limits, and a misconfiguration can quickly exhaust them. Staging issues untrusted certificates with far higher limits.

Swap the issuer uri for the staging directory:

acme_issuer letsencrypt-staging {
    uri        https://acme-staging-v02.api.letsencrypt.org/directory;
    contact    mailto:admin@example.com;
    state_path /var/lib/nginx/acme-staging;
    accept_terms_of_service;
}

Then validate and reload:

sudo nginx -t
sudo systemctl reload nginx

Watch the error log while the module places its order and completes the challenge:

sudo tail -f /var/log/nginx/error.log

Once issuance succeeds, confirm the certificate that NGINX actually serves:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

For a staging certificate, the issuer reads as the Let’s Encrypt staging intermediate. After you switch the uri back to production and reload, the same command shows the trusted Let’s Encrypt production issuer and a 90-day validity window.

Performance Considerations

The module adds no per-request overhead. Certificate provisioning and renewal happen out of band, on the module’s own schedule, not on the hot path of client requests. Serving traffic uses the same in-memory certificate that any ssl_certificate directive would.

The one resource to size correctly is the shared memory zone. The default 256k is comfortable for a handful of certificates. If you provision dozens of hostnames from a single NGINX instance, raise acme_shared_zone accordingly so the module never runs out of space for certificate state.

Security Best Practices

Troubleshooting

directory update failed (connection refused) — NGINX cannot reach the ACME server. Confirm the resolver directive is present in the http block and that outbound HTTPS is allowed. On SELinux systems, allow NGINX to make network connections: sudo setsebool -P httpd_can_network_connect 1.

Challenge fails with connection refused on port 80 — the certificate authority could not reach the challenge path under /.well-known/acme-challenge/. Make sure port 80 is open to the internet and that you have both listen 80; and listen [::]:80;, since validation may arrive over IPv6.

unsupported challenge: dns-01 — the module supports http-01 and tls-alpn-01 only. For DNS-based validation, see the Certbot comparison below.

Certificate not served or TLS handshake fails — confirm the server block on 443 assigns the module variables: ssl_certificate $acme_certificate; and ssl_certificate_key $acme_certificate_key;. These must reference the same issuer named in acme_certificate.

When You Still Need Certbot

The nginx acme module is the cleanest option for standard, publicly reachable certificates. It does not, however, support the dns-01 challenge, which is the only way to obtain wildcard certificates (*.example.com) or certificates for hosts that are not reachable on port 80 or 443.

For those cases, the classic Certbot workflow remains the right tool. If you need wildcard certificates or DNS-based validation through a provider like Cloudflare, follow our companion guide: Free SSL for NGINX with Let’s Encrypt using Certbot. For everything else, the native module is simpler and has fewer moving parts.

Conclusion

A working ACME setup only stays working until the next config change. NGINX upgrades, listener edits, and new vhosts quietly break automatic renewal long before a certificate visibly expires. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).

The nginx acme module turns TLS certificate management into a first-class NGINX feature. You declare an issuer and a certificate, and NGINX provisions, serves, and renews its own Let’s Encrypt certificates with no Certbot, no cron, no Python, and no downtime. For the vast majority of public sites, that is everything you need.

nginx-module-acme is available now from the GetPageSpeed Premium Repository. Subscribe to the repository to install it and unlock 100+ NGINX modules with automatic updates. The module is open source, and you can review it on GitHub at nginx/nginx-acme.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version