Estimated reading time: 7 minutes
Table of contents
If you run a Home Lab, performing an Nginx Proxy Manager upgrade is usually a routine task. But eventually, the itch to upgrade the underlying OS (or the looming End-of-Life date) gets the better of us, turning a simple update into a complex project.
Recently, I decided to tackle a major Nginx Proxy Manager upgrade by moving my existing LXC container to Debian 13 (Trixie). This specific container was originally spun up using the popular Proxmox VE Community Scripts on Debian 12 (Bookworm). My goal was to bring the OS up to Trixie and update NPM from v2.12.6 to v2.13.5.
The “Why didn’t you use Docker?” Elephant in the Room
Now, I know what you’re thinking: “Rámon, why aren’t you just running this in Docker like a normal person?” It’s a fair question. I originally went down the LXC route using the Proxmox VE Community Script ages ago. When that original instance broke about six months back (and I forgot to include it in the scheduled backups, yeah, I know), I was in a rush to get my services back online, so I just quickly spun up a replacement using the same script. It was a “temporary fix” that – as these things often go – became permanent. So, here I am, maintaining a bare-metal-ish LXC container instead of a simple Docker volume.
Why not just reinstall?
Ideally, I would have spun up a fresh container and imported my config. The problem? Nginx Proxy Manager lacks a native export/import function for its configuration, hosts, and SSL certificates. Migrating that data manually is a minefield (but maybe an update like I do here (but succeeded!) as well…). So, I chose the in-place Nginx Proxy Manager upgrade route.
Spoiler alert: It wasn’t just apt upgrade. It turned into a deep dive involving Python virtual environments, legacy library compilation, and patching source code (a very minor patch I have to say).
This post logs the specific errors I hit and the manual steps I took to fix them. I also wrote a script to automate this Nginx Proxy Manager upgrade, which I’ll share at the end, but since every environment is unique, here are the raw details.
Prerequisites:
- Backup: I performed a full backup using Proxmox Backup Server (PBS) before touching anything. Do not skip this.
The Operating System Upgrade
The first step of this Nginx Proxy Manager upgrade was upgrading Debian 12 (Bookworm) to 13 (Trixie). This brings newer packages but also introduces breaking changes.
First, we upgrade. It is always recommended to create a sudo user and run these commands with sudo in front of them:
# Update sources.list to trixie
sed -i 's/bookworm/trixie/g' /etc/apt/sources.list
# Run an update and full-upgrade
apt update
apt full-upgrade -y
# A reboot is highly recommended
The Impact:
- Python: System Python jumped from 3.11 to 3.13.
- PCRE: The legacy
libpcre3library (PCRE 8.x) was removed/obsoleted in favor oflibpcre2(PCRE 10.x).
Note on the Naming: If you find it odd thatlibpcre3is older thanlibpcre2, you aren’t alone. The numbering is counter-intuitive because PCRE2 is the name of the new, re-architected major version (10.x), not simply the next release. Meanwhile,libpcre3was the Debian package name for the older PCRE 8.x series. - Node.js: The manually installed Node.js v16 became dangerously outdated.
After the reboot, chaos ensued. Here is how I fixed it, piece by piece.
Troubleshooting the Nginx Proxy Manager Upgrade
Issue A: The Certbot Crash Loop (Python Mismatch)
The Symptom:
The NPM backend service refused to start. The logs were screaming about missing Python modules.
The Cause:
NPM uses a Python virtual environment (/opt/certbot) to run Certbot for SSL renewal. This venv was created with Python 3.11. When Debian upgraded to Python 3.13, all the binary links and shared libraries inside that venv broke instantly.
The Solution:
I had to nuke the old environment and rebuild it with the new system Python (note that I’m only using the Cloudflare plugin, didn’t bother to install other plugins e.g. certbot-dns-digitalocean, certbot-dns-google etc.).
# Remove the broken environment
rm -rf /opt/certbot
# Create a fresh venv
python3 -m venv /opt/certbot
# Upgrade pip to avoid compatibility issues
/opt/certbot/bin/pip install --upgrade pip
# Reinstall Certbot and the Cloudflare plugin
/opt/certbot/bin/pip install certbot certbot-dns-cloudflare
# Ensure the system uses the new binary
ln -sf /opt/certbot/bin/certbot /usr/bin/certbot
Issue B: OpenResty & The “PCRE Hell”
Update (June 2026):
On a current OpenResty, this “PCRE Hell” is no longer necessary. The official docker-nginx-full image is now built on Debian Trixie and pins OpenResty 1.29.2.5, building straight against the system libpcre2-dev with no legacy PCRE 8.45 compilation at all. I have since upgraded this very LXC to 1.29.2.5: just drop the--with-pcre=/tmp/pcre-8.45flag from the./configurecommand (keep--with-pcre-jit) and modern Nginx auto-detects PCRE2.
The steps below remain accurate for the older OpenResty 1.27.1.2 I originally built.
The Context:
Nginx Proxy Manager relies on OpenResty, a souped-up version of Nginx. The existing binary was linked against libraries that no longer existed in Debian Trixie. I had to compile it from source.
Attempt 1: The Missing Libraries
First, the configure script failed because it couldn’t find the PCRE library (libpcre3-dev), which has been dropped in Trixie.
Attempt 2: Forcing PCRE 2
I tried to force it to use the modern libpcre2, but the Nginx core (specifically the http_rewrite_module) still relies on legacy symbols like pcre_version that simply don’t exist in PCRE 2.
The Final Solution:
Since Trixie doesn’t provide the legacy library, I had to compile PCRE from source and link it statically into OpenResty.
First, install the necessary build tools:
apt update
apt install -y build-essential libpcre2-dev libssl-dev zlib1g-dev wget curl git
Then, download the legacy PCRE source:
cd /tmp
wget -q https://sourceforge.net/projects/pcre/files/pcre/8.45/pcre-8.45.tar.gz
tar -xzf pcre-8.45.tar.gz
Next, download OpenResty:
wget -q https://openresty.org/download/openresty-1.27.1.2.tar.gz
tar -xzf openresty-1.27.1.2.tar.gz
cd openresty-1.27.1.2
Finally, configure and build OpenResty, pointing it to the local PCRE source:
# Ensure paths are set correctly for LuaJIT
export PATH="$PATH:/sbin:/usr/sbin"
./configure \
--with-pcre="/tmp/pcre-8.45" \
--with-pcre-jit \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-http_auth_request_module \
--with-http_v2_module \
--with-http_dav_module \
--with-http_slice_module \
--with-threads \
--with-http_addition_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_sub_module \
--with-stream \
--with-stream_ssl_module \
--with-stream_ssl_preread_module
make -j"$(nproc)"
make install
Issue C: Node.js Version Incompatibility
The Symptom:
Building the NPM frontend failed immediately with EBADENGINE.
The Cause:
Nginx Proxy Manager v2.13.5 requires Node.js v20+. My old container had v16 manually installed in /usr/local/bin.
The Solution:
Debian Trixie actually ships with a modern Node.js (v20.19+), so I could switch to the system package.
# Install the system package
apt install nodejs npm
# Remove the old manual binaries that might conflict
rm -f /usr/local/bin/node /usr/local/bin/npm /usr/local/bin/npx
# Create symlinks because the NPM service file hardcodes /usr/local/bin
ln -sf /usr/bin/node /usr/local/bin/node
ln -sf /usr/bin/npm /usr/local/bin/npm
Issue D: Manual Build & The “v2.0.0” Version Glitch
The Context:
Since we are upgrading in place, we need to manually fetch the new source code, build the frontend, and deploy the backend.
The Glitch:
The GitHub source code often has a placeholder version (2.0.0) in package.json. The real version number is usually injected by their CI/CD pipeline. If you build it locally without patching this, your footer will forever say “v2.0.0”.
The Solution:
Here is the full process to download, patch, build, and deploy the update.
1. Download and Prepare Source:
# Define the version we want
NPM_TAG="v2.13.5"
# Download source
wget -q -O npm.tar.gz https://github.com/NginxProxyManager/nginx-proxy-manager/archive/refs/tags/${NPM_TAG}.tar.gz
tar -xzf npm.tar.gz
cd "nginx-proxy-manager-${NPM_TAG#v}"
# PATCH THE VERSION: Replace placeholder with real version
sed -i "s/\"version\": \"2.0.0\"/\"version\": \"${NPM_TAG#v}\"/" package.json
sed -i "s/\"version\": \"2.0.0\"/\"version\": \"${NPM_TAG#v}\"/" frontend/package.json
sed -i "s/\"version\": \"2.0.0\"/\"version\": \"${NPM_TAG#v}\"/" backend/package.json
2. Build the Frontend:
cd frontend
npm install
npm run locale-compile
npm run build
cd ..
3. Deploy to Production:
# Backup config
cp /app/config/production.json /tmp/production.json.bak
# Clean old app files (but keep config folder)
find /app -mindepth 1 ! -regex "^/app/config\(/.*\)?$" -delete
# Copy new backend files
cp -r backend/* /app/
# Restore config
mv /tmp/production.json.bak /app/config/production.json
# Copy built frontend
mkdir -p /app/frontend
cp -r frontend/dist/* /app/frontend/
# Install backend dependencies
cd /app
npm install --production
Issue E: Service Name Confusion
The Symptom: Running systemctl restart nginx failed with Unit nginx.service not found. Even after using the correct openresty service, it failed with Address already in use errors (bind failed on 0.0.0.0:80).
The Cause: Two things were happening:
- The Proxmox community script uses
openrestyas the service name, notnginx. - The old
nginxprocess from before the upgrade was still running in the background, holding onto port 80 and 443. The new service couldn’t start because the ports were occupied by this “zombie” process.
The Solution: I had to identify the rogue process, kill it, and then restart the service.
# Find the process holding port 80
lsof -i :80
# If 'killall' is missing, install it (required for minimal installs)
apt install -y psmisc
# Kill the old nginx processes
killall nginx
# Restart the services
systemctl restart npm
systemctl restart openresty

Issue F: Internal Error (Missing Nginx Config)
The Symptom: When creating or updating a Proxy Host in the UI, an “Internal Error” occurred. The backend logs showed:
nginx: [emerg] open() "/usr/local/openresty/nginx/conf/conf.d/include/proxy.conf" failed (2: No such file or directory)
The Cause: The compiled OpenResty installation installs default Nginx configs, but Nginx Proxy Manager relies on specific included configuration files (like proxy.conf, ssl-ciphers.conf) that are typically provided in the docker/rootfs directory of the source. These were not copied during the manual deployment.
The Solution: Copy the missing configuration files from the source docker/rootfs to the OpenResty configuration directory.
cp -r docker/rootfs/etc/nginx/conf.d/include /usr/local/openresty/nginx/conf/conf.d/
systemctl reload openresty
Issue G: Logrotate Group Error
The Symptom: Logs showed: error: /etc/logrotate.d/nginx-proxy-manager:2 unknown group 'npm'.
The Cause: The logrotate configuration expects the npm group to exist, but the system runs Nginx as root (or the user/group was not created).
The Solution: Update the logrotate configuration to use root.
sed -i 's/su npm npm/su root root/g' /etc/logrotate.d/nginx-proxy-manager
Automating the Nginx Proxy Manager Upgrade
If the steps above look like a headache you’d rather avoid, I wrapped this entire Nginx Proxy Manager upgrade journey into a single bash script. It automates the OS upgrade prompts, the legacy PCRE compilation, the Python repair, and the Node.js cleanup.
This script is designed specifically for Debian Bookworm containers created with the Proxmox community script, but as always, backup your container first, this might not work for you!
wget https://raw.githubusercontent.com/ramonvanraaij/Scripts/refs/heads/main/linux/Debian/upgrade_npm_trixie.sh
chmod +x upgrade_npm_trixie.sh
sudo ./upgrade_npm_trixie.sh
Final Thoughts
This Nginx Proxy Manager upgrade reinforced a valuable lesson: “Convenience scripts are great for setup, but maintenance is where the real work happens.” Moving from Bookworm to Trixie broke almost every dependency NPM had, but fixing it provided a great look under the hood of how the application is stitched together.
If you are stuck on an old version or just want your OS to be current, give the script a try (or follow the manual steps above). It solves the OpenResty & The “PCRE Hell” so you don’t have to!
Buy me a coffee 🙂
If you found this post helpful, informative, or if it saved or made you some money, consider buying me a coffee. Your support means a lot and motivates me to keep writing.
You can do so via bunq.me (bunq, iDeal, Bankcontact and Credit- or Debit cards) or PayPal (PayPal and Credit- or Debit cards). Thank you!
Disclaimer
The blog posts, guides, and scripts provided on this website are for informational and educational purposes only. They are provided “as is” and without any warranty of any kind, either express or implied, including, but not limited to, the implied warranties of merchantability, fitness for a particular purpose, or non-infringement.
By using the information or scripts from this blog, you agree that I am not liable for any direct, indirect, incidental, consequential, or any other damages or losses arising from the use of or inability to use the information, scripts, or instructions contained herein. You assume full responsibility for any and all risks associated with the use of this content.
The blog posts, guides, and pages may contain referral/affiliate links. If you make a purchase through these links, I may receive a commission at no additional cost to you.
Appreciate the effort put into this post. It answers several common questions in a clear way.
Thanks for the compliment! 🙂
Thanks for the effort! I ended up with a working system but also an error:
[INFO] Backing up installation to /app_backup_20260307_043544…
[INFO] Downloading NPM v2.13.5…
[INFO] Patching version numbers…
sed: -e expression #1, char 40: unterminated `s’ command
Hi Lennart, thanks for reading and your comment!
I solved the issue in the script, I forgot sed’s closing delimiter -_-
Also fixed that in the blog post, you can see the changes here, you could do those manually:
https://github.com/ramonvanraaij/Scripts/commit/152b0b674c0450e99ed770b310f70768b64d3644
Hello,
tried your script to upgrade my lxc. The script leve me with the login screen refreshing constantly until an error about a corrupted API appeared.
Then I followed your post, step by step.
At the “Finally, configure and build OpenResty, pointing it to the local PCRE source:” I got the error –with-pcre=”../pcre-8.45″ as invalid.
I entered the pcre folder and did the ./configure, make and make install.
At this point the rest ./configure worked and compiled and the login page was estable, could enter and all seems ok.
Do you think I must do the rest of the process or the mix script + compile pcre + compile rest is safe?
Thanks for the tutorial. has been months I wanted to have all my lxc on trixie, this was the only one still on bookworm. Best wishes!
Hi, thanks for reading and for giving the script a try! 🙂
Your approach is perfectly valid. By installing PCRE 8.45 as a system library, you gave OpenResty exactly what it needed – the legacy PCRE 1 symbols. The only difference with my approach is static vs. dynamic linking.
Both work fine.
The
--with-pcreerror you hit was likely caused by the relative path (../pcre-8.45) not resolving from your working directory. That path needs to be relative to the OpenResty source directory, which is easyto get wrong. Using an absolute path like
/tmp/pcre-8.45is more reliable – I’ll update the post to reflect that.Since the script failed partway through and you finished manually, I’d verify a few things to be safe:
1. Try creating or editing a Proxy Host. If you get an “Internal Error”, the Nginx config includes are missing (Issue F in the post):
cp -r docker/rootfs/etc/nginx/conf.d/include /usr/local/openresty/nginx/conf/conf.d/systemctl reload openresty
2. Check Certbot (
/opt/certbot/bin/certbot --version). If it errors out, the Python 3.13 venv needs recreating – also in the post.3. Verify PCRE is found at runtime (since yours is dynamically linked):
ldd /usr/local/openresty/nginx/sbin/nginx | grep pcreIf it shows “not found”, a quick
ldconfigshould fix it.One friendly side note – I hope you had a snapshot or backup before going in! 🙂 If not, now that things are working, this would be a great moment to take one. A known-good snapshot after a successful upgrade is worth its weight in
gold.
If proxy hosts work and Certbot is healthy, you’re good. Congrats on getting all your LXCs to Trixie! 🙂