This guide sets up TLS certificates on Debian Stable and Ubuntu LTS with nginx-acme, NGINX's official ACME module.
#1. What is nginx-acme?
nginx-acme is an official NGINX module that implements ACME. It lets NGINX request and renew TLS certificates without a separate ACME client.
If you have used Caddy, the idea will feel familiar: certificate automation lives in the web server config.
The module is implemented in Rust (unlike NGINX itself, which is written in C) and supports the following standards:
In my testing, it has behaved well enough for production use. Still watch your logs, because certificate automation is one of those things you only notice when it stops working.
#2. Install N.WTF (NGINX with nginx-acme)
This guide uses N.WTF, packaged by m.ac. Its nginx-extras package includes nginx-acme.
The commands below use sudo. If you prefer a root shell, run sudo -i first and drop sudo from the commands.
Install the required packages:
sudo apt update
sudo apt upgrade -y
sudo apt install curl vim wget gnupg dpkg apt-transport-https lsb-release ca-certificates
Import the N.WTF signing key and add the APT repository:
curl -sSL https://n.wtf/public.key | sudo gpg --dearmor -o /usr/share/keyrings/n.wtf.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/n.wtf.gpg] https://mirror-cdn.xtom.com/sb/nginx/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/n.wtf.list
On Debian, extrepo can add the same repository:
sudo apt update
sudo apt install extrepo -y
sudo extrepo enable n.wtf
Install NGINX:
sudo apt update
sudo apt install nginx-extras -y
#3. Prepare the certificate directory
Create a directory for ACME state and certificates. This example uses /var/cache/nginx/letsencrypt:
sudo mkdir -p /var/cache/nginx/letsencrypt
sudo chown 33:33 /var/cache/nginx -R
Make sure your domain's A and AAAA records point to this server before you continue.
#4. Configure an NGINX site with a www redirect
This example assumes:
- Your domains are
example.comandwww.example.com - You want
www.example.comto redirect toexample.com - Your contact email is
user@example.com
Edit /etc/nginx/sites-enabled/default and use this configuration:
resolver 8.8.8.8:53 ipv6=off valid=5s; # Or replace to your preferred DNS resolver
acme_issuer letsencrypt {
uri https://acme-v02.api.letsencrypt.org/directory;
contact user@example.com;
state_path /var/cache/nginx/letsencrypt;
accept_terms_of_service;
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
ssl_verify on;
}
acme_shared_zone zone=ngx_acme_shared:1M;
server {
# Listen on port 80 for ACME HTTP-01 validation and HTTP->HTTPS redirects
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# Let nginx-acme handle ACME HTTP-01 challenges.
# Do not block /.well-known/acme-challenge/ here.
location /.well-known/acme-challenge/ {
# nginx-acme serves this internally.
# Keeping the block empty avoids accidental 404s.
}
# Redirect all other HTTP requests to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
# Standard TLS listening
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
# HTTP/2
http2 on;
# HTTP/3 (QUIC)
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
add_header X-Protocol $server_protocol always;
server_name example.com;
root /var/www/html;
index index.html;
# Modern TLS settings
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_prefer_server_ciphers off;
# Enable certificate automation for this server block
acme_certificate letsencrypt;
ssl_certificate $acme_certificate;
ssl_certificate_key $acme_certificate_key;
# Avoid parsing the certificate on every request
ssl_certificate_cache max=2;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
listen 443 quic;
listen [::]:443 quic;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
add_header X-Protocol $server_protocol always;
server_name www.example.com;
return 301 https://example.com$request_uri;
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_prefer_server_ciphers off;
acme_certificate letsencrypt;
ssl_certificate $acme_certificate;
ssl_certificate_key $acme_certificate_key;
ssl_certificate_cache max=2;
}
Test the configuration and reload NGINX:
sudo nginx -t
sudo nginx -s reload
#5. Verify certificate issuance
After NGINX reloads, nginx-acme requests a certificate. Watch the access log while it validates:
sudo tail -f /var/log/nginx/access.log
You should see requests to /.well-known/acme-challenge/ from Let's Encrypt validation servers. A few seconds later, https://example.com/ should answer with a valid certificate.
Example log entries:
23.178.112.210 - - [15/Jan/2026:16:08:18 +0000] "GET /.well-known/acme-challenge/blablablablablablablablablablablablablablab HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)"
34.212.137.78 - - [15/Jan/2026:16:08:18 +0000] "GET /.well-known/acme-challenge/blablablablablablablablablablablablablablab HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)"
#6. IP address certificates with the shortlived profile
To request a certificate for an IP address, add this line inside the acme_issuer letsencrypt {} block:
profile shortlived;
Also set server_name to the full IP address. You cannot use server_name _ for an IP certificate.