---
title: "Issue Let's Encrypt certificates automatically with nginx-acme on Debian and Ubuntu"
tags: ['Debian', 'Ubuntu', 'Nginx', 'nginx-acme', 'Let''s Encrypt', 'SSL']
published: 2026-01-15 18:22:52
updated: 2026-01-15 18:23:05
excerpt: "Set up nginx-acme on Debian or Ubuntu, configure NGINX virtual hosts, and let NGINX request and renew Let's Encrypt certificates."
---
> This file is the raw Markdown source of an m.ac post; the canonical version lives at https://m.ac/nginx-acme-auto-ssl-debian-ubuntu/
> The full index of posts and pages is at https://m.ac/llms.txt — read the index first before crawling further.

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](https://github.com/nginx/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](https://caddyserver.com/), 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:

-   [RFC 8555 (ACME)](https://datatracker.ietf.org/doc/html/rfc8555)
-   [RFC 8737 (TLS-ALPN-01)](https://datatracker.ietf.org/doc/html/rfc8737)
-   [RFC 8738 (IP identifiers)](https://datatracker.ietf.org/doc/html/rfc8738)
-   [draft-ietf-acme-profiles](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/)

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](https://n.wtf/), packaged by [m.ac](https://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:

```bash
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:

```bash
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](https://m.ac/use-extrepo-add-third-party-repositories-debian/) can add the same repository:

```bash
sudo apt update
sudo apt install extrepo -y
sudo extrepo enable n.wtf
```

Install NGINX:

```bash
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`:

```bash
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.com` and `www.example.com`
-   You want `www.example.com` to redirect to `example.com`
-   Your contact email is `user@example.com`

Edit `/etc/nginx/sites-enabled/default` and use this configuration:

```nginx
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:

```bash
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:

```bash
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:

```log
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:

```nginx
profile     shortlived;
```

Also set `server_name` to the full IP address. You cannot use `server_name _` for an IP certificate.
