Let's Connect

A server rack with network cabling, representing TLS termination and HTTPS infrastructure on a production web server

Certbot makes nginx certbot ssl setup feel like a solved problem — one command, free HTTPS, done. Then two server blocks fight over the same port, a 2 a.m. renewal silently fails, or the ACME challenge 404s and you cannot issue at all. Almost every one of these is a config conflict, not a Certbot bug. The fix is the same shape every time: validate the config, look at what is actually issued, then resolve the specific clash. Before touching anything, I run these two commands — they tell me the truth about the current state.

bash
# Does nginx even parse? Catches the conflicts below before you reload.
sudo nginx -t

# What certs exist, for which names, and where do the files actually live?
sudo certbot certificates

`certbot certificates` is the one most people skip, and it answers half the support questions on its own: the exact domains on each cert, the expiry date, and the real paths under `/etc/letsencrypt/live/<name>/`. If the cert covers `example.com` but not `www.example.com`, no amount of nginx tweaking fixes that — you reissue. Get those two outputs clean first, then move to the specific conflict.

How do I issue and renew a certificate the right way?

Issue (or expand) a cert and let the nginx plugin wire it into your server block in one shot. List every hostname the cert should cover with repeated `-d` flags — the apex and the `www` are two different names, and a cert that lacks one throws browser warnings on that host.

bash
# Issue + auto-configure nginx for both apex and www
sudo certbot --nginx -d example.com -d www.example.com

# Rehearse the renewal without touching the live cert or hitting rate limits
sudo certbot renew --dry-run

# Confirm the auto-renew timer is actually scheduled
systemctl list-timers | grep certbot

Renewal is automatic via `certbot.timer`, a systemd timer that fires twice a day. On its own, `certbot renew` only acts on certs that are actually near expiry — for the standard 90-day Let's Encrypt cert, current Certbot renews once less than a third of the lifetime remains, i.e. inside the last 30 days. The mistake I see is assuming "it issued fine" means "it will renew fine" — those are different code paths. The `--dry-run` exercises the real renewal logic against the staging server, so it surfaces a broken ACME path months before the cert actually expires. If `list-timers` shows no `certbot.timer`, nothing is renewing anything and you are on a countdown to an outage.

Why does nginx -t say "a duplicate default server for 0.0.0.0:443"?

This one stops a reload dead. When Certbot adds TLS to a second site, it can copy `listen 443 ssl default_server` into that block — but only one server block per address:port is allowed to be the default. With two declaring it, `nginx -t` fails:

bash
nginx: [emerg] a duplicate default server for 0.0.0.0:443 in /etc/nginx/sites-enabled/site-b:5
nginx: configuration file /etc/nginx/nginx.conf test failed

Keep `default_server` on exactly one block (your real catch-all or first site) and strip the keyword from the others. The other blocks still listen on 443 — they just stop claiming to be the default. The corrected listener looks like this:

/etc/nginx/sites-available/example.com
server {
    # default_server removed — only ONE block owns the default per port
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;

    root /var/www/example.com/public;
    index index.html;
}

Why does the ACME http-01 challenge 404 or fail?

Let's Encrypt validates domain control by fetching a token from `http://your-domain/.well-known/acme-challenge/<token>` over port 80. If your port-80 block is a blanket `return 301 https://...` redirect, or a greedy catch-all, that request gets bounced or swallowed before Certbot's temporary file is served — and validation 404s. The fix is to serve the challenge path explicitly, with a higher-priority `^~` location, before the redirect:

/etc/nginx/sites-available/example.com (port 80)
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Serve the ACME challenge first — ^~ beats the regex/redirect below
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
        default_type "text/plain";
    }

    # Everything else goes to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

Config is only half of it: port 80 has to be reachable from the public internet, not just open in nginx. On a cloud host this bites people constantly — the OS firewall (ufw, firewalld) or the cloud security group is dropping inbound 80. Before you blame nginx, confirm from an outside machine that `curl -I http://your-domain/` actually reaches the box; the HTTP-01 check needs inbound 80 open, full stop.

A terminal window showing server logs and command output during a TLS certificate troubleshooting session
When the ACME challenge fails, the nginx access log for port 80 tells you immediately whether the request even reached the challenge location — or got redirected away.

Why did my custom SSL settings disappear after a renewal?

Certbot owns the `ssl_certificate` and `ssl_certificate_key` lines it wrote, and it marks them with `# managed by Certbot`. If you hand-edit cipher suites, HSTS, or protocol settings inside that managed region, a future `certbot --nginx` run can rewrite the block and wipe your changes — and you find out when a security scan flags weak TLS that you thought you had fixed. Keep Certbot's lines as-is and put your hardening in a separate include so reconfigures never touch it.

  • Leave the `ssl_certificate*` lines exactly as Certbot generated them — never edit inside the `# managed by Certbot` region.
  • Put custom directives (HSTS, `ssl_protocols`, ciphers, OCSP stapling) in your own file, e.g. `/etc/nginx/snippets/ssl-params.conf`, and `include` it in the server block.
  • Reference the Certbot-shipped `/etc/letsencrypt/options-ssl-nginx.conf` for a sane baseline rather than copying random configs off the internet.
  • After every change, run `nginx -t` and confirm `certbot renew --dry-run` still passes before you walk away.
If you are hand-editing the lines Certbot wrote, you are not configuring TLS — you are scheduling a future outage.Md Raihan Hasan

How do I get a wildcard certificate for *.example.com?

You cannot do it with HTTP-01. That challenge proves control of a single hostname by serving a file, and there is no file you can serve that proves you own every possible subdomain. Wildcards require the DNS-01 challenge, where Certbot proves control by writing a `_acme-challenge` TXT record. For automation you point Certbot at a DNS plugin that has API access to your zone (Route 53, Cloudflare, and others), so it can create and tear down that record unattended:

bash
# Wildcard via DNS-01 using the Route 53 plugin (needs AWS API creds in scope)
sudo certbot certonly \
  --dns-route53 \
  -d "example.com" -d "*.example.com"

# Then renewal is hands-off via the same plugin — verify it:
sudo certbot renew --dry-run

The trade-off is that the DNS plugin needs scoped credentials to your zone, which is one more secret to manage on the box. If you are wiring this into a broader cloud setup, I cover where these credentials and the TLS termination fit in my Laravel on AWS production architecture walkthrough.

What is the safe way to apply the change?

Test, then reload — never blind-restart. `nginx -t` parses your full config and refuses to proceed on the duplicate-default and challenge-routing errors above, so it is your gate. Reload (not restart) swaps in the new config gracefully without dropping live connections; a restart tears the master process down and kills in-flight requests for no reason.

bash
# Validate, and only reload if the test passes
sudo nginx -t && sudo systemctl reload nginx

Every conflict in this list comes back to the same discipline: read the actual state with `nginx -t` and `certbot certificates` before you assume, fix the one specific clash, and reload behind a passing config test. Do that and Certbot goes back to being the boring, automated thing it is supposed to be — issuing and renewing quietly in the background while you work on something that actually deserves your attention. The day to discover a broken renewal is during a `--dry-run`, not when a customer screenshots the expired-certificate warning.