[GH-ISSUE #3624] The error was: expected /etc/letsencrypt/live/npm-2/cert.pem to be a symlink #2398

Open
opened 2026-02-26 07:35:25 +03:00 by kerem · 5 comments
Owner

Originally created by @Schmandre on GitHub (Mar 12, 2024).
Original GitHub issue: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/3624

Checklist

  • Have you pulled and found the error with jc21/nginx-proxy-manager:latest docker image?
    • Yes
  • Are you sure you're not using someone else's docker image?
    • Yes
  • Have you searched for similar issues (both open and closed)?
    • Yes

Describe the bug

Nginx Proxy Manager Version

To Reproduce
Steps to reproduce the behavior:
I really dont know. Was working everythime before. Just after moving from old system to new (files wasnt moved, completly new config) the error occures at every renewalphase.
I fixed with deleting all files and create everything new from zero. But that is very ... shit :)

Expected behavior

Working cert. renew

Screenshots

I like logs more, so here are logs from the error

2024-03-12 08:37:46,405:DEBUG:certbot._internal.main:certbot version: 2.9.0                                                          2024-03-12 08:37:46,406:DEBUG:certbot._internal.main:Location of certbot entry point: /opt/certbot/bin/certbot                       2024-03-12 08:37:46,406:DEBUG:certbot._internal.main:Arguments: ['--force-renewal', '--config', '/etc/letsencrypt.ini', '--work-dir'>2024-03-12 08:37:46,406:DEBUG:certbot._internal.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#dns-cloudflare,PluginEntry>2024-03-12 08:37:46,418:DEBUG:certbot._internal.log:Root logging level set at 30
2024-03-12 08:37:46,418:DEBUG:certbot._internal.display.obj:Notifying user: Processing /etc/letsencrypt/renewal/npm-2.conf
2024-03-12 08:37:46,419:ERROR:certbot._internal.renewal:Renewal configuration file /etc/letsencrypt/renewal/npm-2.conf is broken.
2024-03-12 08:37:46,419:ERROR:certbot._internal.renewal:The error was: expected /etc/letsencrypt/live/npm-2/cert.pem to be a symlink
Skipping.
2024-03-12 08:37:46,420:DEBUG:certbot._internal.renewal:Traceback was:
Traceback (most recent call last):
  File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/renewal.py", line 76, in reconstitute
    renewal_candidate = storage.RenewableCert(full_path, config)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/storage.py", line 510, in __init__
    self._check_symlinks()
  File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/storage.py", line 589, in _check_symlinks
    raise errors.CertStorageError(
certbot.errors.CertStorageError: expected /etc/letsencrypt/live/npm-2/cert.pem to be a symlink

2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: No renewals were attempted.
2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user:
Additionally, the following renewal configurations were invalid:
2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user:   /etc/letsencrypt/renewal/npm-2.conf (parsefail)
2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - >2024-03-12 08:37:46,420:DEBUG:certbot._internal.log:Exiting abnormally:
Traceback (most recent call last):
  File "/opt/certbot/bin/certbot", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/opt/certbot/lib/python3.11/site-packages/certbot/main.py", line 19, in main
    return internal_main.main(cli_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/main.py", line 1894, in main
    return config.func(config, plugins)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/main.py", line 1642, in renew
    renewed_domains, failed_domains = renewal.handle_renewal_request(config)
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/renewal.py", line 568, in handle_renewal_request
    raise errors.Error(
certbot.errors.Error: 0 renew failure(s), 1 parse failure(s)
2024-03-12 08:37:46,421:ERROR:certbot._internal.log:0 renew failure(s), 1 parse failure(s)

image

Operating System

Desktop PC System with dedicated hardware

Additional context

Docker compose file

version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      # These ports are in format <host-port>:<container-port>
      - '80:80' # Public HTTP Port
      - '443:443' # Public HTTPS Port
      - '81:81' # Admin Web Port
      # Add any other Stream port you want to expose
      # - '21:21' # FTP

    # Uncomment the next line if you uncomment anything in the section
    # environment:
      # Uncomment this if you want to change the location of
      # the SQLite DB file within the container
      # DB_SQLITE_FILE: "/data/database.sqlite"

      # Uncomment this if IPv6 is not enabled on your host
      # DISABLE_IPV6: 'true'

    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
Originally created by @Schmandre on GitHub (Mar 12, 2024). Original GitHub issue: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/3624 <!-- Are you in the right place? - If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit. - If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask. - If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.* --> **Checklist** - Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image? - Yes - Are you sure you're not using someone else's docker image? - Yes - Have you searched for similar issues (both open and closed)? - Yes **Describe the bug** <!-- A clear and concise description of what the bug is. --> **Nginx Proxy Manager Version** <!-- What version of Nginx Proxy Manager is reported on the login page? --> **To Reproduce** Steps to reproduce the behavior: I really dont know. Was working everythime before. Just after moving from old system to new (files wasnt moved, completly new config) the error occures at every renewalphase. I fixed with deleting all files and create everything new from zero. But that is very ... shit :) **Expected behavior** <!-- A clear and concise description of what you expected to happen. --> Working cert. renew **Screenshots** <!-- If applicable, add screenshots to help explain your problem. --> I like logs more, so here are logs from the error ``` 2024-03-12 08:37:46,405:DEBUG:certbot._internal.main:certbot version: 2.9.0 2024-03-12 08:37:46,406:DEBUG:certbot._internal.main:Location of certbot entry point: /opt/certbot/bin/certbot 2024-03-12 08:37:46,406:DEBUG:certbot._internal.main:Arguments: ['--force-renewal', '--config', '/etc/letsencrypt.ini', '--work-dir'>2024-03-12 08:37:46,406:DEBUG:certbot._internal.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#dns-cloudflare,PluginEntry>2024-03-12 08:37:46,418:DEBUG:certbot._internal.log:Root logging level set at 30 2024-03-12 08:37:46,418:DEBUG:certbot._internal.display.obj:Notifying user: Processing /etc/letsencrypt/renewal/npm-2.conf 2024-03-12 08:37:46,419:ERROR:certbot._internal.renewal:Renewal configuration file /etc/letsencrypt/renewal/npm-2.conf is broken. 2024-03-12 08:37:46,419:ERROR:certbot._internal.renewal:The error was: expected /etc/letsencrypt/live/npm-2/cert.pem to be a symlink Skipping. 2024-03-12 08:37:46,420:DEBUG:certbot._internal.renewal:Traceback was: Traceback (most recent call last): File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/renewal.py", line 76, in reconstitute renewal_candidate = storage.RenewableCert(full_path, config) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/storage.py", line 510, in __init__ self._check_symlinks() File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/storage.py", line 589, in _check_symlinks raise errors.CertStorageError( certbot.errors.CertStorageError: expected /etc/letsencrypt/live/npm-2/cert.pem to be a symlink 2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: No renewals were attempted. 2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: Additionally, the following renewal configurations were invalid: 2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: /etc/letsencrypt/renewal/npm-2.conf (parsefail) 2024-03-12 08:37:46,420:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - >2024-03-12 08:37:46,420:DEBUG:certbot._internal.log:Exiting abnormally: Traceback (most recent call last): File "/opt/certbot/bin/certbot", line 8, in <module> sys.exit(main()) ^^^^^^ File "/opt/certbot/lib/python3.11/site-packages/certbot/main.py", line 19, in main return internal_main.main(cli_args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/main.py", line 1894, in main return config.func(config, plugins) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/main.py", line 1642, in renew renewed_domains, failed_domains = renewal.handle_renewal_request(config) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/certbot/lib/python3.11/site-packages/certbot/_internal/renewal.py", line 568, in handle_renewal_request raise errors.Error( certbot.errors.Error: 0 renew failure(s), 1 parse failure(s) 2024-03-12 08:37:46,421:ERROR:certbot._internal.log:0 renew failure(s), 1 parse failure(s) ``` ![image](https://github.com/NginxProxyManager/nginx-proxy-manager/assets/47451170/9d053059-1b1b-48c7-a290-1583de16650b) **Operating System** <!-- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error. --> Desktop PC System with dedicated hardware **Additional context** <!-- Add any other context about the problem here, docker version, browser version, logs if applicable to the problem. Too much info is better than too little. --> Docker compose file ``` version: '3.8' services: app: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: # These ports are in format <host-port>:<container-port> - '80:80' # Public HTTP Port - '443:443' # Public HTTPS Port - '81:81' # Admin Web Port # Add any other Stream port you want to expose # - '21:21' # FTP # Uncomment the next line if you uncomment anything in the section # environment: # Uncomment this if you want to change the location of # the SQLite DB file within the container # DB_SQLITE_FILE: "/data/database.sqlite" # Uncomment this if IPv6 is not enabled on your host # DISABLE_IPV6: 'true' volumes: - ./data:/data - ./letsencrypt:/etc/letsencrypt ```
Author
Owner

@feerlessleadr commented on GitHub (Mar 26, 2024):

I'm having this exact same issue. Any luck?

My log is identical to yours

<!-- gh-comment-id:2020608922 --> @feerlessleadr commented on GitHub (Mar 26, 2024): I'm having this exact same issue. Any luck? My log is identical to yours
Author
Owner

@daniel-widrick commented on GitHub (May 10, 2024):

The work around for this issue is to simply delete the certs from the interface and then renable ssl on the hosts...

You can rebuild the cert structure manually but ideally this would be handled automatically. It's not unreasonable for these symlinks to get copied as files as part of a backup/migration solution and graceful handling would be nice.

<!-- gh-comment-id:2105201327 --> @daniel-widrick commented on GitHub (May 10, 2024): The work around for this issue is to simply delete the certs from the interface and then renable ssl on the hosts... You can rebuild the cert structure manually but ideally this would be handled automatically. It's not unreasonable for these symlinks to get copied as files as part of a backup/migration solution and graceful handling would be nice.
Author
Owner

@dampfhamm3r commented on GitHub (Jun 1, 2024):

I was in the same situation. In my case this issue arised after migrating NPM from a Linux container to a new docker instance.

I've copied all cert.pem/chain.pem/fullchain.pem/privkey.pem from each proxy host to the docker instance.
This messed up the symlinks. I've then created all the needed symlink manually and can now renew the certs without any issues.

For a proxy host npm-4 you need the following:

# ls -lhaF live/npm-4/ | grep ^l
lrwxrwxrwx 1 root root  30 Jun  1 09:28 cert.pem -> ../../archive/npm-4/cert11.pem
lrwxrwxrwx 1 root root  31 Jun  1 09:28 chain.pem -> ../../archive/npm-4/chain11.pem
lrwxrwxrwx 1 root root  35 Jun  1 09:28 fullchain.pem -> ../../archive/npm-4/fullchain11.pem
lrwxrwxrwx 1 root root  33 Jun  1 09:28 privkey.pem -> ../../archive/npm-4/privkey11.pem

The destination file names must contain a number (eg. cert11.pem)! If the symlink points to a file without the number, the renewal will not work.

<!-- gh-comment-id:2143347855 --> @dampfhamm3r commented on GitHub (Jun 1, 2024): I was in the same situation. In my case this issue arised after migrating NPM from a Linux container to a new docker instance. I've copied all cert.pem/chain.pem/fullchain.pem/privkey.pem from each proxy host to the docker instance. This messed up the symlinks. I've then created all the needed symlink manually and can now renew the certs without any issues. For a proxy host `npm-4` you need the following: ``` # ls -lhaF live/npm-4/ | grep ^l lrwxrwxrwx 1 root root 30 Jun 1 09:28 cert.pem -> ../../archive/npm-4/cert11.pem lrwxrwxrwx 1 root root 31 Jun 1 09:28 chain.pem -> ../../archive/npm-4/chain11.pem lrwxrwxrwx 1 root root 35 Jun 1 09:28 fullchain.pem -> ../../archive/npm-4/fullchain11.pem lrwxrwxrwx 1 root root 33 Jun 1 09:28 privkey.pem -> ../../archive/npm-4/privkey11.pem ``` The destination file names must contain a number (eg. cert`11`.pem)! If the symlink points to a file without the number, the renewal will not work.
Author
Owner

@github-actions[bot] commented on GitHub (Dec 30, 2024):

Issue is now considered stale. If you want to keep it open, please comment 👍

<!-- gh-comment-id:2564952416 --> @github-actions[bot] commented on GitHub (Dec 30, 2024): Issue is now considered stale. If you want to keep it open, please comment :+1:
Author
Owner

@gautamsi commented on GitHub (Aug 10, 2025):

I was able to get a script using chatgpt, in case someone needs it.

My case was due to restore, the symlinks were gone, running from inside the npm container works or change the script accordingly

#!/bin/bash
set -euo pipefail

LIVE_DIR="/etc/letsencrypt/live"
ARCHIVE_DIR="/etc/letsencrypt/archive"

FILES=("cert.pem" "privkey.pem" "chain.pem" "fullchain.pem")

for live_subdir in "$LIVE_DIR"/*; do
    [ -d "$live_subdir" ] || continue
    folder_name=$(basename "$live_subdir")
    archive_path="$ARCHIVE_DIR/$folder_name"

    if [ ! -d "$archive_path" ]; then
        echo "⚠️ No archive found for: $folder_name — skipping"
        continue
    fi

    echo "Processing $folder_name..."
    for f in "${FILES[@]}"; do
        base_name="${f%.pem}"   # e.g., cert, privkey, chain, fullchain
        pattern="$archive_path/${base_name}[0-9]*.pem"
        latest_file=$(ls -v $pattern 2>/dev/null | tail -n 1)

        if [ -z "$latest_file" ]; then
            echo "  ❌ No files found for $f in $archive_path"
            continue
        fi

        # Compute relative path from live_subdir to latest_file
        rel_path=$(realpath --relative-to="$live_subdir" "$latest_file")

        # Remove old link or file
        rm -f "$live_subdir/$f"
        # Create new relative symlink
        ln -s "$rel_path" "$live_subdir/$f"
        echo "  ✅ Linked $f$rel_path"
    done
done

echo "Done."
<!-- gh-comment-id:3172875327 --> @gautamsi commented on GitHub (Aug 10, 2025): I was able to get a script using chatgpt, in case someone needs it. My case was due to restore, the symlinks were gone, running from inside the npm container works or change the script accordingly ```sh #!/bin/bash set -euo pipefail LIVE_DIR="/etc/letsencrypt/live" ARCHIVE_DIR="/etc/letsencrypt/archive" FILES=("cert.pem" "privkey.pem" "chain.pem" "fullchain.pem") for live_subdir in "$LIVE_DIR"/*; do [ -d "$live_subdir" ] || continue folder_name=$(basename "$live_subdir") archive_path="$ARCHIVE_DIR/$folder_name" if [ ! -d "$archive_path" ]; then echo "⚠️ No archive found for: $folder_name — skipping" continue fi echo "Processing $folder_name..." for f in "${FILES[@]}"; do base_name="${f%.pem}" # e.g., cert, privkey, chain, fullchain pattern="$archive_path/${base_name}[0-9]*.pem" latest_file=$(ls -v $pattern 2>/dev/null | tail -n 1) if [ -z "$latest_file" ]; then echo " ❌ No files found for $f in $archive_path" continue fi # Compute relative path from live_subdir to latest_file rel_path=$(realpath --relative-to="$live_subdir" "$latest_file") # Remove old link or file rm -f "$live_subdir/$f" # Create new relative symlink ln -s "$rel_path" "$live_subdir/$f" echo " ✅ Linked $f → $rel_path" done done echo "Done." ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/nginx-proxy-manager-NginxProxyManager#2398
No description provided.