Tech Digest

Guide

Why does Nginx Proxy Manager say "Internal Error" when requesting a Let's Encrypt certificate?

Internal Error is not a diagnosis. It is the text Nginx Proxy Manager shows when certbot fails, and the reason is sitting in a log file inside the container. Here is where to read it, and what each line means.

Last reviewed

Why does Nginx Proxy Manager say Internal Error when requesting a Let's Encrypt certificate?

Because certbot, the program NPM runs to get the certificate, failed, and NPM 2.16.0 replaces any error it does not mark as public with the words Internal Error. The real reason is in /data/logs/letsencrypt.log inside the container. Run docker exec npm grep -E 'Detail:|Error|error' /data/logs/letsencrypt.log. The usual causes are port 80 not reaching NPM, a DNS record that is missing or points elsewhere, a name with no public suffix such as .home, a rate limit, a rejected Cloudflare token, or /etc/letsencrypt copied without its symlinks.

"Internal Error" means certbot failed and Nginx Proxy Manager hid the reason. Certbot is the program NPM runs to ask Let's Encrypt for a certificate. When it exits with an error, NPM 2.16.0 shows only those two words. The real reason is in a log file inside the container:

bash
docker exec npm grep -E 'Detail:|Error|error' /data/logs/letsencrypt.log

Replace npm with your container's name. With the standard compose file, the same file is ./data/logs/letsencrypt.log on the host.

The terms this answer turns on#

Internal Error is a placeholder, not a message. NPM's backend has one error handler for every request. If an error is marked public, the handler sends its text to the browser. If not, it sends the words Internal Error instead. When certbot fails, NPM wraps its output in a CommandError, and that type is never public. So the dialog says Internal Error, whatever the cause.

Certbot is the ACME client inside the NPM image. ACME is the protocol Let's Encrypt uses. NPM 2.16.0 ships certbot 5.8.0 and runs it with --logs-dir /data/logs.

A challenge is how Let's Encrypt checks that you control the name. With the HTTP challenge, Let's Encrypt fetches a file from http://your.domain/.well-known/acme-challenge/ on port 80, and NPM must answer. With the DNS challenge, NPM uses your DNS provider's API to add a TXT record, and nothing needs to be open.

letsencrypt.log is certbot's own log. Certbot starts a fresh file on every run and renames the old one to letsencrypt.log.1, then .2, keeping up to 1,000. NPM's renewal timer runs every hour and calls certbot for any certificate within 30 days of expiry. So run ls -lt /data/logs/letsencrypt.log* and pick the file whose time matches your click.

Where the reason is in 2.16.0, and why one place is not enough#

Certbot writes two streams of output. Its non-interactive display sends the per-domain report to standard output: the Identifier, Type and Detail lines. Errors go to the error stream. When certbot fails, NPM keeps only the error stream. That one choice decides what each place shows:

WhereWhat it shows
/data/logs/letsencrypt.logEverything, including the Detail: line for each domain
docker logs npmThe error stream only. For a failed challenge that is just Some challenges have failed.
Browser developer tools, Network tabThe error stream, in the debug.stack of the API response. Only for calls to /api/nginx/certificates, such as a request from the SSL Certificates page
The NPM dialogInternal Error

The developer tools trick is how the reporters of issue #3717 got their text. NPM attaches the error's stack to responses from /api/nginx/certificates, and that stack begins with certbot's error output. The 2.16.0 web interface reads only the message, so you never see it on screen.

Two answers circulate, and both stop too early#

The first answer is that Internal Error is an NPM bug, so update or reinstall NPM. It is not a bug. It is a message that hides the real one. Threads with the exact same words run from issue #579 in 2020 to #3717 and #3719 in 2024. All three were closed for inactivity. Reinstalling changes nothing if port 80 is still closed.

The second answer is that port 80 is closed. That is one cause of several. A .home name, a rate limit or a bad API token gives the same two words. The log tells you in seconds which one you have, so read it before you change the router.

The cause map: what the log says and what to do#

The develop branch of NPM now carries a list of common certbot failures with a plain-language hint for each. That list, the certbot source and the threads above give this map. The last column says whether docker logs shows the clue, or only the log file does.

Log containsWhat it meansFixIn docker logs?
Timeout during connectLet's Encrypt got no answer on port 80Forward port 80 to NPM; open the firewallNo, file only
Connection refusedSomething answered on port 80 and refusedPort 80 reaches the wrong machine or nothing listensNo, file only
Invalid response fromPort 80 reached a server, but not NPM's challenge fileDNS points elsewhere, or another proxy sits in frontNo, file only
NXDOMAIN or DNS problemLet's Encrypt could not look up the nameCreate or fix the A or AAAA recordNo, file only
Domain name does not end with a valid public suffixA name like thuis.homeUse a real domain you ownYes
too many certificates, too many failed authorizations, rateLimitedA Let's Encrypt rate limitWait for the limit to refillYes
9109 with location: and an IPCloudflare token limited to other client IPsAdd the IP to the token, or remove the filterYes
6003, 6111, 9103, 10000, Invalid access tokenCloudflare rejected the tokenNew token with Zone, DNS, Edit on the zoneYes
expected ... to be a symlink/etc/letsencrypt copied without its linksRestore with tar or rsync -a, or recreate the certificateYes
Renewal configuration file ... is brokenThat certificate's files are damaged or missingRestore /etc/letsencrypt, or delete and recreateYes
No new run at that timeThe failure came before certbotRead docker logsYes

The last row matters. Before it runs certbot, NPM tests and reloads its nginx configuration. A DNS challenge also installs the provider's certbot plugin with pip. If either step fails, you get the same Internal Error, but certbot never ran. docker logs then shows nginx's own complaint, or Some plugins failed to install.

On rate limits, the numbers that bite while debugging are small. Let's Encrypt allows 5 failed validations per name, per account, per hour. It allows 5 certificates for the exact same set of names every 7 days. Clicking Save ten times in a row turns a port problem into a rate-limit problem.

The reachability test is not Let's Encrypt#

The reachability test in NPM's certificate dialog does not ask Let's Encrypt anything. NPM writes a file called test-challenge containing Success. Then it sends your domain to site24x7.com's REST API tester. That service fetches the file over plain HTTP from one location. The results map to fixed messages:

  • ok: the file came back with Success.
  • no-host: no server was found at the name.
  • 404 or wrong-data: a server answered, but not NPM.
  • failed: NPM could not reach site24x7 at all.

A pass proves that one place on the internet can reach port 80. Let's Encrypt checks from several. Its April 2024 post says it added validation from Sweden and Singapore. It also says it "will not announce which IP addresses may perform these checks". A geoblocking firewall can pass the site24x7 test and still fail Let's Encrypt. Commenters on #3717 traced their own failures to exactly that, one of them to a UniFi geoblock that logged nothing. The test also covers only the HTTP challenge. It says nothing about a DNS challenge.

A worked case: one request, followed through the logs#

Take an NPM container named npm on a home server. You request a certificate for cloud.example.org with the HTTP challenge, and the dialog says Internal Error.

Start with docker logs --tail 30 npm. Suppose the last warning reads Some challenges have failed. That rules out the token, symlink and rate-limit rows, since those print their own text there. Let's Encrypt could not validate the name.

Now open the file. Suppose grep Detail: /data/logs/letsencrypt.log returns a line ending in Timeout during connect (likely firewall problem). Nothing answered on port 80. Check the router's forward for port 80 to the server, and the host firewall.

Suppose the forward is right and the reachability test says ok. The one place site24x7 checks from can reach you, and Let's Encrypt's other locations cannot. Look for country blocking on the router or firewall. Turn it off, or switch this certificate to a DNS challenge.

Now change one fact. Suppose the certificate uses the Cloudflare DNS challenge, and docker logs shows 9109 Cannot use the access token from location: 203.0.113.7. That is the case that started PR #6004: renewals failed for months after the server's public IP changed. The token has a client IP filter. Add the new IP to the token in Cloudflare, under My Profile, API Tokens, and request again.

Copied /etc/letsencrypt: the migration trap#

NPM's compose file mounts ./letsencrypt at /etc/letsencrypt. Inside, each certificate's live/npm-N/ folder holds four symlinks into archive/. Copy that folder with a tool that turns links into plain files, and certbot refuses to touch it. Its source raises expected .../cert.pem to be a symlink, and the renewal logs Renewal configuration file ... is broken.

It can stay hidden after a move until the first renewal, which NPM attempts only within 30 days of expiry. Copy with tar or rsync -a, which keep links. If the original is gone, delete the certificate in NPM and request it again. Moving a service to a new machine covers moving a whole stack this way.

What changes when the next release ships#

PR #6004, "Show certbot's failure reason in the UI instead of Internal Error", was merged into develop on 4 November 2026. The latest release is still 2.16.0, from 24 September 2026, so check the releases page before you rely on it. Once released, a certbot failure becomes a normal error with certbot's own text, and the dialog shows it. For the causes in its list, a one-line hint goes in front.

It reads the same error stream as before, though. Certbot's source still sends the Detail: lines to standard output. So for a failed challenge, expect the dialog to show Some challenges have failed. and keep reading letsencrypt.log for the reason.

When this answer stops applying#

This page reads the 2.16.0 release and the develop branch as of 5 November 2026, with certbot 5.8.0. Other images that package NPM, such as jlesage/nginx-proxy-manager, use other paths. Older NPM versions wrote certbot's log to /tmp/letsencrypt-log, as the #3717 output shows. The Cloudflare codes come from NPM's hint list, not from Cloudflare's documentation, and other DNS providers print their own errors. Rate limits are Let's Encrypt's current published values and can change. A custom CA set through LE_SERVER will word its errors differently. Uploaded custom certificates do not run certbot at all.

What to do next#

Read letsencrypt.log before you change anything, match the line to the map, and fix only that. Set LE_STAGING=1 while you retry, so failures do not count against production limits, then remove it. If the certificate works but the site behind it now returns 502, Why Nginx Proxy Manager returns 502 Bad Gateway reads that log the same way. For DNS records and split horizon, see DNS for self-hosters. And since renewals fail as quietly as requests, Let's Encrypt 45-day certificates and your proxy explains how NPM's hourly timer renews and how much margin you have.

Questions#

Where is the Let's Encrypt log in Nginx Proxy Manager?

Inside the container at /data/logs/letsencrypt.log. With the standard compose file that is ./data/logs/letsencrypt.log on the host. Certbot starts a new file on every run and moves the old one to letsencrypt.log.1, .2 and so on, keeping up to 1,000. NPM's hourly renewal timer also runs certbot, so check the file times with ls -lt and pick the run that matches the moment you clicked Save.

Why does docker logs only say Some challenges have failed?

Because certbot prints the per-domain reason (the Identifier, Type and Detail lines) to its standard output in non-interactive mode, and NPM keeps only the error stream when certbot fails. The error stream holds just Some challenges have failed. and a pointer to the log file. The Detail line is written to /data/logs/letsencrypt.log, so read that file for any failure during validation.

Will updating Nginx Proxy Manager fix Internal Error?

Not by itself, and not yet. The latest release is 2.16.0, from 24 September 2026. A change that shows certbot's reason in the dialog instead of Internal Error was merged into the develop branch on 4 November 2026 and is not in any release as of 5 November 2026. When it ships, the message will be clearer. The cause, such as a closed port 80, still has to be fixed outside NPM.

The reachability test passes but the certificate still fails. Why?

The test is run by site24x7.com, not Let's Encrypt. NPM asks one site24x7 location to fetch a test file from your domain over plain HTTP. Let's Encrypt validates from several places, and since 2024 that includes servers in Sweden and Singapore. A firewall that geoblocks those countries lets the test through and still fails the real challenge. The test also says nothing about DNS challenges.

Can Nginx Proxy Manager get a Let's Encrypt certificate for a .home or .lan domain?

No. Let's Encrypt only issues for names under a public suffix. A request for thuis.home failed with Cannot issue for "thuis.home": Domain name does not end with a valid public suffix (TLD) in issue #3717. Use a real domain you own, with a DNS challenge if the service is only reachable at home, or a private CA for internal names.

How do I stop hitting Let's Encrypt rate limits while I debug?

Point NPM at the staging server by setting the environment variable LE_STAGING=1, which makes NPM pass --staging to certbot. Staging has significantly higher limits. In production, one account gets only 5 failed validations per name per hour. Staging certificates are not trusted by browsers, so remove the variable, recreate the container and request again once the cause is fixed.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.