---
title: "How to Set Up Certbot on nginx: Free SSL Certificate That Renews Automatically"
description: "How to set up Certbot on nginx for a free Let's Encrypt SSL certificate that renews automatically: install, renewal test, common errors and Cloudflare."
url: "https://carthacker.com/hosting/certbot-nginx/"
author: "Adrian Wadowski"
published: "2026-10-31"
section: "Hosting & Security"
image: "https://carthacker.com/wp-content/uploads/certbot-nginx-free-ssl-certificate.jpg"
publisher: "CartHacker (carthacker.com)"
language: "en"
---

# How to Set Up Certbot on nginx: Free SSL Certificate That Renews Automatically

By [Adrian Wadowski](https://carthacker.com/author/adrian/), building online stores since 1998. Published October 31, 2026. 30 min read.

A Let's Encrypt certificate lasts 90 days today, and on February 16, 2028 the default drops to 45 days. Since June 4, 2025, Let's Encrypt doesn't email anyone before a certificate expires. So if nothing else checks your certificate, the first sign of a failed renewal is Chrome showing a shopper "Your connection is not private" instead of your product page.

![Chrome warning page on a phone: Your connection is not private. Attackers might be trying to steal your information from expired.x1.test-certs.letsencrypt.org. NET::ERR_CERT_DATE_INVALID, a blue Back to safety button and an Advanced link](https://carthacker.com/wp-content/uploads/chrome-expired-certificate-warning-1.jpg)
*What a shopper sees when the certificate has expired: Chrome opening Let's Encrypt's own test site for an expired certificate*

Here's how to set up **Certbot on nginx** for a free SSL certificate that renews automatically, step by step, on a WooCommerce store that runs on its own Ubuntu server. Getting the first certificate takes one command. The real work is making sure it still renews three months later, and that you'll know if it doesn't.

I ran the whole setup on test machines, broke the renewal on purpose and moved the clock forward to see when three Certbot versions renew a 45-day certificate. If your store runs on Shopify, Wix or a host that manages HTTPS for you, the host renews the certificate and you don't need any of this. On your own server, you'll end up with a working certificate, a renewal you've tested and a daily check that warns you 10 days before the certificate expires.

> **My test setup:** Everything here ran in Docker on October 28, 2026, on Ubuntu 24.04 (nginx 1.24, Certbot 2.9.0) and Ubuntu 26.04 (nginx 1.28, Certbot 4.0.0), plus Certbot 5.8.0, the version in the snap. A test machine in a lab has no public domain, so the certificates came from Pebble, a small test version of the Let's Encrypt server that Let's Encrypt publishes. Every certificate my lab issued is a test certificate, and no browser trusts it. The Certbot part (the commands, the nginx edits, the timer, the renewal decisions) is the same software your server runs, and where Pebble's error message is different from the real Let's Encrypt server's, I quote both.

## Quick setup: four commands

On Ubuntu, as root (or after `sudo -i`), with nginx already serving your domain on port 80, these four commands get the certificate, test the renewal and show when the renewal timer runs next:

```bash
apt install certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com
certbot renew --dry-run
systemctl list-timers | grep certbot
```

Then add a daily check of the certificate your server really sends, which doesn't depend on Certbot (step 6). The steps after this explain each command and what to do when one of them fails.

## Step 1: Check DNS, port 80 and your nginx server block

With the nginx plugin, Certbot uses the HTTP-01 challenge to prove that you control the domain: Let's Encrypt requests a short token file from your domain at /.well-known/acme-challenge/, and **this request always starts on port 80**, because the ACME standard doesn't let a client choose another port. Before the first run, check that:

- The domain points at this server. `dig +short example.com` and `dig +short www.example.com` should both print your server's IP address.
- Ports 80 and 443 are open. On Ubuntu's firewall that's `ufw allow 'Nginx Full'` (the nginx package includes that profile, for ports 80 and 443). Many VPS providers also have a firewall in their control panel.
- An nginx server block lists every name you want on the certificate in its server_name line.

This is the server block I started with, a basic WordPress one for example.com and www.example.com:

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}
```

For a store, [How to set up nginx for WooCommerce](https://carthacker.com/hosting/nginx-wordpress-config/) shows the complete config with the rules for the cart and the checkout.

## Step 2: Install Certbot for nginx: apt or snap

The Certbot version depends on where you install it from, and each version renews at a different time (the last section of this article has my test):

| Installed from | Certbot | Released |
| --- | --- | --- |
| Ubuntu 24.04 LTS, apt | 2.9.0 | February 8, 2024 |
| Ubuntu 26.04 LTS and Debian 13, apt | 4.0.0 | April 7, 2025 |
| snap (latest/stable), PyPI, Docker | 5.8.0 | September 1, 2026 |

Versions from my test machines and the certbot snap page, release dates from PyPI and the Certbot changelog, October 28, 2026.

The EFF, which develops Certbot, recommends the snap. Its instructions say to remove any apt version first, so you never have two Certbots with two timers:

```bash
# the snap, as the EFF instructions give it
apt remove certbot
snap install --classic certbot
ln -s /snap/bin/certbot /usr/local/bin/certbot

# or Ubuntu's own packages (the nginx plugin is python3-certbot-nginx)
apt install certbot python3-certbot-nginx
```

Which one should you use? On Ubuntu 24.04 I'd use the snap, because Certbot 2.9.0 is three major versions behind, has no ARI (the renewal time Let's Encrypt suggests for each certificate) and can't request a certificate profile at all. On Ubuntu 26.04 either one works. I didn't run the snap itself in my lab (snapd doesn't run inside Docker), so my 5.8.0 results come from the same release installed from PyPI, the Python package index.

## Step 3: Get the certificate with certbot --nginx

This is the run on my Ubuntu 24.04 test machine. In my command, `--server` makes Certbot use the test server, and --agree-tos, -m and --no-eff-email answer Certbot's questions (terms, email, EFF mailing list), so Certbot doesn't ask them. On your server, type the short command from the quick setup and answer the questions on screen.

```bash
root@server:~# certbot --nginx -d example.com -d www.example.com --server https://pebble:14000/dir --agree-tos -m admin@example.com --no-eff-email
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.
Requesting a certificate for example.com and www.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2027-01-26.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for example.com to /etc/nginx/sites-enabled/example.com
Successfully deployed certificate for www.example.com to /etc/nginx/sites-enabled/example.com
Congratulations! You have successfully enabled HTTPS on https://example.com and https://www.example.com
...
```

Output from my test machine (I removed the donation lines at the end). The certificate expires in 90 days because I set up the test server with today's Let's Encrypt default.

Certbot still asks for an email address, but Let's Encrypt doesn't send expiry warnings to it anymore. Instead, it adds the address to the general mailing list of ISRG, the nonprofit that runs Let's Encrypt.

Here's the same nginx file after the run (I removed Certbot's empty lines, nothing else):

```nginx
server {
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 404; # managed by Certbot
}
```

**Certbot changed your server block to listen on port 443 (HTTPS) and added a second server block for port 80 that redirects every visitor to HTTPS.** The ssl_certificate and ssl_certificate_key lines point into /etc/letsencrypt/live/, where every renewal replaces the files under the same names, so nginx never needs a new path. The included options file sets the TLS versions and the ciphers, and a comment in it says they're based on Mozilla's recommendations. Don't edit it: Certbot updates that file itself.

The new port 80 block redirects both names to https and answers "not found" to any other host name that reaches it. During each renewal, the plugin adds two temporary rules to your server blocks, a location block for the token file and a rewrite rule, so Let's Encrypt's request for the token file isn't redirected. Then it removes both. You won't see them in the file afterwards, only in /var/log/letsencrypt/letsencrypt.log.

## Step 4: Turn on HTTP/2

Certbot's listen lines don't turn on HTTP/2, so browsers load your store over the older protocol. On Ubuntu 24.04's nginx, add http2 to both listen lines with the sed command below. The nginx in Ubuntu 26.04 is newer and warns that this listen parameter is deprecated, so there you add one line, `http2 on;`, inside the server block that has the listen 443 lines.

```bash
root@server:~# curl -s -o /dev/null -w "%{http_version}\n" --http2 --cacert /root/pebble-root.pem --resolve example.com:443:127.0.0.1 https://example.com/
1.1
root@server:~# sed -i 's/listen 443 ssl;/listen 443 ssl http2;/; s/listen \[::\]:443 ssl ipv6only=on;/listen [::]:443 ssl http2 ipv6only=on;/' /etc/nginx/sites-available/example.com
root@server:~# nginx -t && systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
root@server:~# curl -s -o /dev/null -w "%{http_version}\n" --http2 --cacert /root/pebble-root.pem --resolve example.com:443:127.0.0.1 https://example.com/
2
```

On my Ubuntu 24.04 test machine. The --cacert and --resolve parts are there because the machine has a test certificate and no public DNS; on your server, leave them out of the curl command. When I forced a renewal afterwards, Certbot kept the http2 change.

HTTPS adds a short delay before the page starts loading: the browser and the server first set up the encryption (the TLS handshake), and a visitor who types http first gets a redirect. Did HTTPS make your store slower? Run a speed test once HTTPS is on.

> **Store Speed Test:** Check how fast your store responds over HTTPS. Paste a product page address and see its three Core Web Vitals scores and the total delay. A free account names the 5 apps that cost the most time, with the fix for your platform. [Test my store](https://carthacker.com/store-speed-test/)

## Step 5: Check that the certificate renews automatically

### The renewal timer

"Certbot has set up a scheduled task" means a systemd timer that comes with the package. Ubuntu's apt package includes it as certbot.timer, the snap installs its own (twice a day at random times, according to its build file), and `systemctl list-timers | grep certbot` shows either one. Here's the apt timer on my Ubuntu 24.04 test machine:

```bash
root@server:~# cat /usr/lib/systemd/system/certbot.timer
[Unit]
Description=Run certbot twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true

[Install]
WantedBy=timers.target
root@server:~# systemctl list-timers certbot.timer
NEXT                        LEFT LAST PASSED UNIT          ACTIVATES
Wed 2026-10-28 16:52:09 UTC  16h -         - certbot.timer certbot.service
```

Each run starts at midnight or noon plus a random delay of up to 12 hours, so not every server contacts Let's Encrypt at the same minute. With Persistent=true, a run that was missed while the server was off happens as soon as the server starts again. The timer starts `certbot -q renew`, which checks the dates and renews only a certificate that's due. The package also installs a cron file in /etc/cron.d/certbot, which does nothing on a server where systemd is running.

### Test the renewal with a dry run

`certbot renew --dry-run` runs a complete renewal against Let's Encrypt's staging server (a test version of the real server) and saves nothing. On my test machine, Certbot's log shows that the dry run contacted the staging server, although my local test server had issued the certificate:

```bash
root@server:~# certbot renew --dry-run
...
Simulating renewal of an existing certificate for example.com and www.example.com
Failed to renew certificate example.com with error: urn:ietf:params:acme:error:rejectedIdentifier :: The server will not issue certificates for the identifier :: Error creating new order :: Cannot issue for "example.com": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy ...
root@server:~# grep -m1 "Sending GET request to https://acme-staging" /var/log/letsencrypt/letsencrypt.log
2026-10-28 00:12:20,572:DEBUG:acme.client:Sending GET request to https://acme-staging-v02.api.letsencrypt.org/directory.
```

The staging server refused example.com because that name is reserved for examples. On your server **this is the right test**: the staging server checks your real domain from the internet, through your real firewall and DNS, and it has its own rate limits, so a failed test doesn't use up the limits of the real Let's Encrypt server. When the dry run works, the last line says "Congratulations, all simulated renewals succeeded". (In my lab I sent the dry run to the test server with --server, and it passed.)

### Reload nginx after each renewal

nginx reads the certificate files when it loads its configuration, and according to its documentation a changed configuration only takes effect after a reload or a restart. With the nginx plugin this is automatic: after every renewal Certbot printed `Reloading nginx server after certificate renewal`. A certificate you get with `certbot certonly` (the webroot method, or DNS-01 behind Cloudflare) is different: Certbot doesn't change nginx for it, so nothing reloads nginx. I tested what happens then:

```bash
before:   file  serial=5DCB05F5F5D33F1D notAfter=Jan 26 00:28:18 2027 GMT
before:   nginx serial=5DCB05F5F5D33F1D notAfter=Jan 26 00:28:18 2027 GMT
renewed:  file  serial=1F1EF423F418E7E5 notAfter=Jan 26 00:28:34 2027 GMT
renewed:  nginx serial=5DCB05F5F5D33F1D notAfter=Jan 26 00:28:18 2027 GMT
reloaded: file  serial=1F1EF423F418E7E5 notAfter=Jan 26 00:28:34 2027 GMT
reloaded: nginx serial=1F1EF423F418E7E5 notAfter=Jan 26 00:28:34 2027 GMT
```

A certonly certificate on my test machine: the serial number on disk and the serial number nginx sends, read with openssl before the renewal, after it, and after `systemctl reload nginx`.

After the renewal, **nginx kept sending the old certificate** while the new one was already on disk. Certbot renews weeks before a certificate expires (about a month early on a 90-day one), so the old certificate keeps working for weeks, and then the site stops working on a day when nobody changed anything. The fix is **a deploy hook**, a script that Certbot runs after each successful renewal:

```bash
root@server:~# cat /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
# Reload nginx after any certificate is renewed, so it serves the new file.
systemctl reload nginx
root@server:~# chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
```

Every executable file in that folder runs after each renewal. `--deploy-hook "systemctl reload nginx"` on the certonly command does the same for one certificate. On my DNS-01 certificate, nginx's log showed the reload right after the renewal.

## Step 6: Check the expiry date yourself every day

`certbot certificates` shows the certificate files on disk:

```bash
root@server:~# certbot certificates
...
  Certificate Name: example.com
    Serial Number: b5f676a2015f15e
    Key Type: ECDSA
    Domains: example.com www.example.com
    Expiry Date: 2027-01-26 00:30:19+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
```

The files on disk aren't always what visitors get, as the reload test showed, so **check the certificate nginx actually sends**. In this command, -connect is the address to connect to (127.0.0.1 on the server itself, your domain from any other computer), and -servername is the site name, which nginx uses to choose the right certificate:

```bash
root@server:~# echo | openssl s_client -connect 127.0.0.1:443 -servername example.com 2>/dev/null | openssl x509 -noout -issuer -enddate
issuer=CN = Pebble Intermediate CA 032aa6
notAfter=Jan 26 00:30:19 2027 GMT
```

On my test machine, so the issuer is the test server; on your server it's Let's Encrypt.

When Cloudflare's proxy is on, a check against your domain shows the certificate Cloudflare sends to visitors, not the one on your server. To read the one on your server, connect to the server's own IP address and keep your domain as the name: `openssl s_client -connect 203.0.113.10:443 -servername example.com`.

This script runs that check and prints a warning when fewer than 10 days are left. On 45-day and longer certificates, 10 days is below every renewal point in my tests, so a warning means renewals have been failing for days:

```bash
#!/bin/sh
# Warn when the certificate a server sends for a name runs out within DAYS days.
# Usage: check-cert.sh  [address] [days]
NAME="$1"; ADDR="${2:-$1}"; DAYS="${3:-10}"
PEM=$(echo | timeout 10 openssl s_client -connect "$ADDR:443" -servername "$NAME" 2>/dev/null | openssl x509 2>/dev/null)
if [ -z "$PEM" ]; then
  echo "WARNING: no certificate from $ADDR for $NAME"; exit 2
fi
END=$(echo "$PEM" | openssl x509 -noout -enddate | cut -d= -f2)
if echo "$PEM" | openssl x509 -noout -checkend $((DAYS * 86400)) >/dev/null; then
  echo "OK: $NAME expires $END"
else
  echo "WARNING: $NAME expires $END, less than $DAYS days away"; exit 1
fi
```

```bash
root@server:~# check-cert.sh example.com 127.0.0.1
OK: example.com expires Jan 26 00:30:19 2027 GMT
root@server:~# check-cert.sh example.com 127.0.0.1 100
WARNING: example.com expires Jan 26 00:30:19 2027 GMT, less than 100 days away
```

On my test machine, with the script saved as /usr/local/bin/check-cert.sh and made executable with `chmod 755`. The second run asks for 100 days to show what a warning looks like. From another computer, leave out the address: `check-cert.sh example.com`. If Cloudflare's proxy is on, add your server's IP address, or the script reads Cloudflare's certificate: `check-cert.sh example.com 203.0.113.10`.

**Run it once a day from a computer that isn't the store's server**: a second server, your own computer, or any cron job that can email you. A check on the server itself stops when the server stops. If you don't want to run anything yourself, Red Sift Certificates Lite is a free hosted service that monitors up to 250 certificates. Let's Encrypt recommended it when it stopped the expiry emails.

Did you change your firewall, your DNS or a Cloudflare setting? Run `certbot renew --dry-run` after each of those changes. They can stop renewals without any error on the site, and the dry run shows the problem the same day.

## Common Certbot errors and how to fix them

These errors stop HTTPS from working on the first run, or make a renewal fail months later. **A failed renewal doesn't break the site at once**: the old certificate keeps working until it expires, and that's how a store can have weeks of failed renewals without any warning. This is what the failed timer looked like on my 24.04 test machine with port 80 blocked (I forced the renewal so it ran right away):

```bash
root@server:~# systemctl --failed --plain --no-legend
certbot.service loaded failed failed Certbot
root@server:~# journalctl -u certbot.service
...
Oct 28 00:29:23 server certbot[1643]: Failed to renew certificate example.com with error: Some challenges have failed.
Oct 28 00:29:23 server certbot[1643]: All renewals failed. The following certificates could not be renewed:
Oct 28 00:29:23 server certbot[1643]:   /etc/letsencrypt/live/example.com/fullchain.pem (failure)
Oct 28 00:29:23 server systemd[1]: certbot.service: Failed with result 'exit-code'.
```

### Timeout: a firewall blocks port 80

```bash
Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
  Domain: example.com
  Type:   connection
  Detail: Get "http://example.com:80/.well-known/acme-challenge/iCXFHATrWtYmeiw26J63NhRDqYZI5b4Phtu50_xcHfs": context deadline exceeded (Client.Timeout exceeded while awaiting headers)

Hint: The Certificate Authority failed to verify the temporary nginx configuration changes made by Certbot. Ensure the listed domains point to this nginx server and that it is accessible from the internet.
```

My test machine, with an iptables rule that blocks port 80. The Detail line is the test server's wording.

The real Let's Encrypt server prints your IP address, the token URL and `Timeout during connect (likely firewall problem)`. **Open port 80 for everyone**, in ufw and in your provider's firewall. Let's Encrypt checks from several network locations and doesn't publish their IP addresses, so you can't open port 80 only for Let's Encrypt. A redirect from http to https is fine, because Let's Encrypt follows up to 10 redirects to ports 80 or 443.

### 404 or NXDOMAIN: the domain points at another server or has no DNS record

After you move the store to a new server, the A record can still point at the old server, and the old server answers the token request with its own 404:

```bash
  Domain: shop.example.com
  Type:   unauthorized
  Detail: Non-200 status code from HTTP: http://shop.example.com:80/.well-known/acme-challenge/sTKEj1OuUAprz9-fG7SIrCBRjAd1EINP2kyURitBBZs returned 404
```

On my test machine, with shop.example.com pointed at a second nginx machine. Let's Encrypt's own wording is `Invalid response from http://...: 404`, and a name with no DNS record at all gives `DNS problem: NXDOMAIN looking up A for example.com - check that a DNS record exists for this domain`. Fix the A record, wait until `dig +short` shows the new IP address, and run Certbot again.

Check the AAAA record (the IPv6 address) too. When a domain has one, Let's Encrypt tries IPv6 first and uses IPv4 only when the IPv6 connection fails. So an old IPv6 address that still answers makes the challenge fail in the same way.

### Could not install certificate: no server block with that name

```bash
Successfully received certificate.
...
Deploying certificate
Could not install certificate
NEXT STEPS:
- The certificate was saved, but could not be installed (installer: nginx). After fixing the error shown below, try installing it again by running:
  certbot install --cert-name blog.example.com

Could not automatically find a matching server block for blog.example.com. Set the `server_name` directive to use the Nginx installer.
```

Certbot 2.9.0 on my test machine, for blog.example.com, a name that no server block lists.

Look at the first line: **the certificate was issued anyway**, and it counts toward your weekly limit. Add the name to the right server_name line, run `nginx -t`, then run the certbot install command that Certbot printed.

### Too many certificates: the weekly limit

Let's Encrypt allows **5 certificates for the exact same set of names every 7 days** and 50 per registered domain, and it doesn't raise the first limit for anyone. Five failed challenges for one name within an hour also block that name for your account for up to an hour. A user posted the real message on the Let's Encrypt forum in May 2026:

```bash
too many certificates (5) already issued for this exact set of identifiers in the last 168h0m0s, retry after 2026-05-24 10:52:56 UTC
```

You'll reach this limit if you delete /etc/letsencrypt and start again a few times in one week while you fix nginx. While you're testing, use `certbot certonly --nginx --dry-run -d example.com -d www.example.com` (the short `certbot --nginx` command doesn't accept --dry-run). It uses the staging server and saves nothing. Run the real command only after the dry run passes.

## Certbot behind Cloudflare

When Cloudflare's proxy is on for your domain (the orange cloud icon in Cloudflare's DNS settings), your domain resolves to Cloudflare's IP addresses, and Let's Encrypt's request for the token file reaches Cloudflare first. Cloudflare sends it on to your server, so the HTTP-01 challenge can still work, but two Cloudflare settings change what your server receives. Both are in the Cloudflare dashboard: the encryption mode is on the **SSL/TLS Overview** page, and Always Use HTTPS is on the **Edge Certificates** page.

In Flexible mode, Cloudflare connects to your server over plain HTTP. The port 80 block that Certbot wrote answers with a redirect to https, Cloudflare requests the page over HTTP again, and visitors get ERR_TOO_MANY_REDIRECTS. Cloudflare's own troubleshooting page describes this redirect loop and the fix: **set the encryption mode to Full or Full (strict)**, or remove the redirect from the server.

Full (strict) checks that your server's certificate hasn't expired, comes from a public certificate authority or from Cloudflare's Origin CA, and has the host name in it. If your certificate fails that check, every visitor gets Cloudflare's error 526, even though Cloudflare's own certificate is fine. So with Full (strict), an expired Let's Encrypt certificate on your server makes the store unreachable for every visitor who comes through Cloudflare.

Always Use HTTPS redirects every http request at Cloudflare, including the request for the token file. Let's Encrypt follows that redirect without checking the certificate on the https address, and in Full or Full (strict) mode Cloudflare then connects to your server over HTTPS. According to the Let's Encrypt and Cloudflare documentation (I can't test this part against the real Cloudflare in a lab), the challenge works while your certificate is valid, and in Full (strict) it fails once the certificate has expired, which is exactly when you need the renewal. The DNS-01 challenge avoids all of these redirects.

### DNS-01 with the Cloudflare plugin and a limited API token

DNS-01 proves that you control the domain with a TXT record at _acme-challenge.example.com, so Let's Encrypt never has to connect to your server. It's also the only challenge that gives wildcard certificates. The downside, in Let's Encrypt's own words: "Keeping API credentials on your web server is risky." So give the Cloudflare API token that Certbot uses as few rights as possible:

1. In the Cloudflare dashboard, go to **My Profile > API Tokens**, select **Create Token** and use the **Edit zone DNS** template.
2. In the resources part of the form, choose only your store's domain, then press **Continue to summary** and **Create Token**. The plugin needs Zone:DNS:Edit for the zones you want certificates for, nothing else. Never use the Global API Key: Cloudflare says it "has access to all of a user's resources".
3. Save the API token on the server, in a file only root can read.

The credentials file has two lines. Put your API token in place of the example token from the plugin's documentation:

```ini
# Cloudflare API token used by Certbot
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
```

If you installed the snap, skip the apt line and run `snap set certbot trust-plugin-with-root=ok` and `snap install certbot-dns-cloudflare` instead, because the snap's Certbot doesn't use plugins installed with apt.

```bash
apt install python3-certbot-dns-cloudflare
mkdir -p /root/.secrets
nano /root/.secrets/cloudflare.ini        # paste the two lines
chmod 600 /root/.secrets/cloudflare.ini
certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/cloudflare.ini -d example.com -d '*.example.com' --deploy-hook "systemctl reload nginx"
```

Certbot doesn't edit nginx for this certificate. Put the two paths it prints into your ssl_certificate and ssl_certificate_key lines, then run `nginx -t` and `systemctl reload nginx`.

With Ubuntu's own Certbot, Ubuntu's own plugin package works: on Ubuntu 24.04 it installs a cloudflare library that's new enough for API tokens. If you skip chmod 600, Certbot prints the first warning in this output (the second error is from my test machine, which had no internet connection):

```bash
Requesting a certificate for example.com
Unsafe permissions on credentials configuration file: /root/.secrets/cloudflare.ini
Unable to determine zone_id for example.com using zone names: ['example.com', 'com']. The error from Cloudflare was: 0 HTTPSConnectionPool(host='api.cloudflare.com', port=443): Max retries exceeded ...
```

On my test machine, with a fake token and outgoing traffic blocked, so nothing reached Cloudflare. After chmod 600 the warning was gone.

In my test, DNS-01 issued **a certificate for example.com and *.example.com with port 80 closed**. Certbot creates one TXT value for the domain and one for the wildcard, both at the same _acme-challenge name, so you'll see two records there while Certbot runs. The Cloudflare plugin waits 10 seconds for DNS by default (--dns-cloudflare-propagation-seconds changes it).

### Cloudflare's Origin CA certificate instead

Cloudflare also issues its own origin certificates, valid for up to 15 years, and Full (strict) accepts them. Only Cloudflare trusts them: if you pause Cloudflare or turn off the proxy for that name, visitors get a certificate error. I'd still use Let's Encrypt with DNS-01 on a store, because that certificate stays valid in browsers if you ever have to turn Cloudflare off, and Certbot renews it with the same timer.

## Certbot in Docker and docker compose

The official image, certbot/certbot, runs Certbot once and exits, and its output says so: `Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality.` The image has no timer. In the setup I tested, nginx runs in its own container, and the nginx and certbot containers share the webroot and /etc/letsencrypt (I removed the lab-only parts, the test network and the test server's root certificate):

```yaml
services:
  nginx:
    image: nginx:1.28
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./certbot/conf:/etc/letsencrypt:ro

  certbot:
    image: certbot/certbot:v5.8.0
    volumes:
      - ./certbot/www:/var/www/certbot
      - ./certbot/conf:/etc/letsencrypt
```

The nginx server block for port 80 serves `location /.well-known/acme-challenge/ { root /var/www/certbot; }` and redirects everything else. Start nginx with only that server block (`docker compose up -d nginx`) and run the certonly line from the folder with compose.yml. Then add the 443 block that points at /etc/letsencrypt/live/example.com/ (nginx won't start while a certificate file is missing, so it can't be there earlier) and run the reload line:

```bash
docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d example.com -d www.example.com
# now add the 443 block to ./nginx/conf.d, then:
docker compose exec nginx nginx -s reload

# renewal, from the server's crontab: twice a day, then a reload
17 3,15 * * * cd /srv/shop && docker compose run --rm certbot renew -q && docker compose exec -T nginx nginx -s reload
```

I ran the certonly, renew and reload commands by hand on my test machine; the cron line runs the last two on a schedule. The certbot container can't reach the nginx container to reload it, so the server's own crontab runs both commands. A reload is graceful (nginx starts new worker processes and lets the old ones finish their requests), so a reload twice a day doesn't stop a page that a shopper is loading. Certbot also publishes images with DNS plugins included, such as certbot/dns-cloudflare.

## Let's Encrypt certificate lifetime changes and their dates

Here's what Let's Encrypt has changed since 2025, and what each change means on your server:

| Date | Change at Let's Encrypt | What it means on your server |
| --- | --- | --- |
| June 4, 2025 | Expiration emails stopped; stored email addresses were deleted | No warning arrives anymore. You need your own expiry check (step 6) |
| May 7 and August 6, 2025 | OCSP addresses removed from new certificates, then the OCSP servers switched off; revocation is published only in CRLs (lists of revoked certificates) | ssl_stapling lines in nginx do nothing, and nginx -t warns `"ssl_stapling" ignored, no OCSP responder URL in the certificate`; you can delete those lines |
| January 15, 2026 | Anyone can request six-day certificates (160 hours) with the shortlived profile | Use them only with a Certbot that has ARI (4.1.0 or newer) |
| May 13, 2026 | The opt-in tlsserver profile issues 45-day certificates | You can already request 45-day certificates to test your server |
| February 10, 2027 | The default profile issues 64-day certificates; a passed challenge is accepted for 10 days, down from 30 | Certbot renews every 5 to 6 weeks instead of every 2 months |
| February 16, 2028 | The default goes to 45 days; a passed challenge is accepted for 7 hours | Every renewal has to pass the challenge again |
| March 15, 2029 | Industry rules limit every public certificate to 47 days | Paid certificates get the same limit |

From the Let's Encrypt posts and pages under Sources; the 47-day rule comes from a CA/Browser Forum ballot adopted in April 2025. The OCSP warning is from nginx -t on my test machine, with a certificate that has no OCSP address, like every certificate Let's Encrypt issues today.

The shorter lifetime breaks renewals that run on a fixed schedule. Let's Encrypt's own announcement says **"renewing at a hardcoded interval of 60 days will no longer be sufficient"**, so a cron job that runs `certbot renew --force-renewal` every two months will stop working once certificates last 45 days. Let's Encrypt recommends ARI (ACME Renewal Information: Let's Encrypt tells each client when to renew), and a client without ARI should renew when a third of the lifetime is left. Renewals that follow ARI don't count toward any rate limit.

## When each Certbot version renews a 45-day certificate

Certbot 2.9.0 renews when fewer than 30 days are left. According to the changelog, version 4.0.0 renews when a third of the lifetime is left, and 4.1.0 added ARI. To see what that means for shorter certificates, I issued a 90-day and a 45-day test certificate to each version, and a six-day one to the two versions that can request it. Then I moved the clock forward with faketime (a tool that fakes the date for one command), one day at a time (one hour at a time for the six-day certificate), and ran `certbot renew` until Certbot printed "Renewing an existing certificate".

![Chart of the day each Certbot version first tried to renew a 45-day test certificate issued October 28, 2026: Certbot 2.9.0 from Ubuntu 24.04 on day 15, with 29 days left; Certbot 4.0.0 from Ubuntu 26.04 on day 15, with 29 days left; Certbot 5.8.0, the snap's version, on day 31, with 13 days left, inside the renewal window the test server suggested](https://carthacker.com/wp-content/uploads/certbot-renewal-day-45-day-certificate-1.png)
*The first renewal attempt on a 45-day certificate from my local test server, by Certbot version*

| Certbot | 90-day certificate | 45-day certificate | Six-day certificate |
| --- | --- | --- | --- |
| 2.9.0 (Ubuntu 24.04 apt) | day 60, 29 days left | day 15, 29 days left | can't request one |
| 4.0.0 (Ubuntu 26.04 apt) | day 60, 29 days left | day 15, 29 days left | after 1 hour, 158 hours left |
| 5.8.0 (the snap's version) | day 61, 28 days left | day 31, 13 days left | after 80 hours, 79 hours left |

The day of the first renewal attempt, counted from October 28, 2026, the day each certificate was issued. I ran one local test server for each lifetime. Certbot 5.8.0 followed the renewal window that the test server suggested through ARI.

Certbot 4.0.0 doesn't do what its changelog says. **The Certbot in Ubuntu 26.04 still renews a 45-day certificate with a month left**, on the same day as 2.9.0. The reason is in its code: it still adds the old 30-day setting (renew_before_expiry) to every certificate and checks it first. Debian 13's package has the same code (I read it there but didn't run the renewal test on Debian 13), and Certbot 5.8.0 no longer adds that setting.

Is renewing early a problem? On 45-day certificates it means a new certificate every two weeks, well below Let's Encrypt's limit of five for the same names per week. Renewing early does no harm. A renewal that comes too late is what shows shoppers the warning page.

The six-day certificate is different. Certbot 4.0.0 tried to renew it one hour after it was issued, so the twice-a-day timer would request a new certificate on every run, and Let's Encrypt refuses the sixth one within a week. **Don't use the shortlived profile with Ubuntu 26.04's Certbot.** Version 5.8.0 waited until half the lifetime had passed, and its ARI renewals don't count toward the limits.

In my tests, all three versions tried to renew a 90-day certificate in time, and the two older ones tried to renew a 45-day certificate earlier than they needed to. So with 90-day and 45-day certificates, any of the three versions renews in time, and only the six-day certificate needs Certbot 5.8.0.

## Sources

- Let's Encrypt: [Ending support for expiration notification emails](https://letsencrypt.org/2025/01/22/ending-expiration-emails/) (January 22, 2025) and [Expiration notification service has ended](https://letsencrypt.org/2025/06/26/expiration-notification-service-has-ended) (June 26, 2025); [Ending OCSP support in 2025](https://letsencrypt.org/2024/12/05/ending-ocsp/) (December 5, 2024) and [OCSP service has reached end of life](https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life) (August 6, 2025).
- Let's Encrypt: [Decreasing certificate lifetimes to 45 days](https://letsencrypt.org/2025/12/02/from-90-to-45) (December 2, 2025), [64-day certificate lifetimes coming February 2027](https://letsencrypt.org/2026/10/07/64-day-certs) (October 7, 2026), [Short-lived and IP address certificates generally available](https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability) (January 15, 2026), [Profiles](https://letsencrypt.org/docs/profiles/), [Certificate lifetimes](https://letsencrypt.org/docs/cert-lifetimes/) and the [Integration guide](https://letsencrypt.org/docs/integration-guide/) (ARI, renewal at a third of the lifetime).
- Let's Encrypt: [Challenge types](https://letsencrypt.org/docs/challenge-types/), [Rate limits](https://letsencrypt.org/docs/rate-limits/), [Staging environment](https://letsencrypt.org/docs/staging-environment/), the test sites on [Chains of trust](https://letsencrypt.org/certificates/), and [Pebble](https://github.com/letsencrypt/pebble), the test server used in my lab. Error texts of the production server from the [Boulder source code](https://github.com/letsencrypt/boulder) (va/va.go, va/http.go, bdns/problem.go); the rate limit message from a [Let's Encrypt community thread](https://community.letsencrypt.org/t/rate-limiting-by-certbot-issues/247491) (May 23, 2026).
- CA/Browser Forum: [Ballot SC-081v3](https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/) (April 11, 2025).
- Certbot: [User guide](https://eff-certbot.readthedocs.io/en/stable/using.html) (nginx plugin, renewal, hooks), [changelog](https://github.com/certbot/certbot/blob/main/certbot/CHANGELOG.md) (4.0.0 April 7, 2025; 4.1.0 June 10, 2025), [EFF instructions for nginx](https://certbot.eff.org/instructions?ws=nginx&os=snap), [certbot-dns-cloudflare](https://certbot-dns-cloudflare.readthedocs.io/en/stable/), [the certbot snap](https://snapcraft.io/certbot) and [the Ubuntu packages](https://launchpad.net/ubuntu/+source/python-certbot).
- Cloudflare: [Encryption modes](https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/), [Full (strict)](https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/full-strict/), [ERR_TOO_MANY_REDIRECTS](https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/), [Error 526](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-526/), [Always Use HTTPS](https://developers.cloudflare.com/ssl/edge-certificates/additional-options/always-use-https/), [Origin CA certificates](https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/) and [their 15-year validity](https://developers.cloudflare.com/use-cases/solutions/encrypt-all-keep-site-secure/), [Create an API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and [Global API key](https://developers.cloudflare.com/fundamentals/api/get-started/keys/).
- nginx: [Beginner's guide](https://nginx.org/en/docs/beginners_guide.html) (reloading the configuration) and [ngx_http_v2_module](https://nginx.org/en/docs/http/ngx_http_v2_module.html) (http2 on, added in nginx 1.25.1).

---

## Cite this guide

Wadowski, A. (2026, October 31). *How to Set Up Certbot on nginx: Free SSL Certificate That Renews Automatically*. CartHacker. https://carthacker.com/hosting/certbot-nginx/

## Free tools from CartHacker

- [Store Speed Test](https://carthacker.com/store-speed-test/): Find the apps that slow your product page down.
- [Core Web Vitals Monitoring](https://carthacker.com/plugins/core-web-vitals-monitoring/): Monitor your store's Core Web Vitals every day, for free.
- [Trust Seals & Badges](https://carthacker.com/trust-badges/): Show shoppers it's safe to buy, right under Add to cart.
- [QR Code Generator](https://carthacker.com/qr-code-generator/): Make a QR code you can still change after printing.
- [PDF Tools](https://carthacker.com/pdf-tools/): Free online PDF tools that run in your browser: merge, split, convert, sign and protect PDFs. 39 tools, no upload, no watermark, no sign-up.
- [CartHacker Premium](https://carthacker.com/premium/): one subscription, monthly or yearly, that upgrades every tool at once:
  - 50 trust badges in 30 languages, switched automatically
  - 180 days of Core Web Vitals, per page
  - All 39 PDF tools, unlimited and never uploaded
  - 200 QR codes, tracked and editable after printing
  - No ads on any guide or tool
  - Every new guide 2 weeks before the public
  - Product Feed Manager and new tools included
- Every quota and price, tool by tool: https://carthacker.com/llms.txt
