nginx-proxy-manager

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

2posts · 30d
Nov 30 posts
  1. Stirling-PDF Docker Setup: Login, OCR Languages and Updates --- title: "Stirling-PDF Docker Setup: Login, OCR Languages and Updates" published: true description: "Run Stirling-PDF 3.2 with Docker Compose: pin the image, replace the admin/stirling default login, add OCR languages like Spanish and update safely." tags: selfhosted, docker, pdf, ocr canonical_url: deploylark.com --- Stirling-PDF is a self-hosted toolbox for PDFs. Merge, split, compress, sign, redact, convert and OCR run on your own server, so the files never go to an online PDF site. On October 17, 2026, the GitHub repository had about 94,300 stars, and the stirlingtools/stirling-pdf image had about 16.3 million pulls on Docker Hub, plus 22.5 million under the older frooodle/s-pdf name, which still gets new releases. This guide runs Stirling-PDF 3.2.0 with Docker Compose and fixes the parts the quick start leaves to you. The default login is shown on the login page to anyone who opens it, extra OCR languages need a volume, and uploads through nginx fail above 1 MB until you change one setting. I ran every step on a Debian 13 test server with Docker 29.9.1, OCR'd scans I generated in English and Spanish, and upgraded a second install from 3.1.0 to 3.2.0. TL;DR: - Use the official compose file with a pinned tag such as docker.stirlingpdf.com/stirlingtools/stirling-pdf:3.2.0, publish the port on 127.0.0.1:8080, and keep mem_limit: 4g. - A fresh install creates admin with the password stirling and prints both on the login page. Set SECURITY_INITIALLOGIN_USERNAME and SECURITY_INITIALLOGIN_PASSWORD before the first start, or log in and change the password right away. - The standard image can OCR English, German, French, Portuguese and Simplified Chinese. For any other language, put its .traineddata file in the folder mounted at /usr/share/tessdata and restart. - Behind nginx, set client_max_body_size 2000M. To update, change the tag, then run docker compose pull and docker compose up -d. What Stirling-PDF runs and which image to pick What's in the container Stirling-PDF is one container. It holds a Java web app plus the command-line tools that do the heavy work. The diagram shows the setup this guide builds: !Diagram of the Stirling-PDF setup on one Docker host. Your browser, or curl with an API token, reaches the server through an SSH tunnel or a reverse proxy with HTTPS and client_max_body_size 2000M. The proxy connects to the stirling-pdf 3.2.0 container, published on 127.0.0.1:8080 only. The container runs the Java app with login on, OCRmyPDF 17.13.0 with Tesseract 5.3.4, and LibreOffice 26.2.6.3 started on first use, with mem_limit 4g sizing the Java heap. Below it, the ./stirling-data bind mounts hold configs (settings.yml, database, keys), storage, tessdata for extra OCR languages, logs, customFiles and pipeline. The 3.2.0 container logs its tool versions at startup. Mine listed OpenJDK 25.0.4, OCRmyPDF 17.13.0, Tesseract 5.3.4, LibreOffice 26.2.6.3, Ghostscript 10.08.0, qpdf 12.4.2 and calibre 9.15.0. LibreOffice doesn't start until the first Office conversion, so it uses no memory until you need it. One feature is off out of the box. The log said Missing dependency: rar - Disabling group: rar (Affected features: PDF To Cbr), which matches the docs. Settings, the user database and generated keys go in /configs. On first start the app also creates credential-encryption.key there and logs a warning to back it up, because losing it makes stored integration secrets unrecoverable. Standard, fat or ultra-lite The Docker install docs list three images: | Tag | Login | What's included | Suggested memory | | --- | --- | --- | --- | | 3.2.0 | Yes | OCR plus Office, HTML and ebook conversion | 4 GB | | 3.2.0-fat | Yes | The same plus a bundled AI engine, off until you turn it on | 6 GB | | 3.2.0-ultra-lite | No | Core PDF tools only, no OCR or conversions | 2 GB | Use the standard image. It's the one I tested end to end. The compressed download on Docker Hub was 1.17 GB for 3.2.0 and 394 MB for 3.2.0-ultra-lite. On disk they took 3.58 GB and 1.04 GB. Ultra-lite is lighter, and my test showed what you give up. It used 357 MiB of RAM after starting, against 795 MiB to 1.08 GiB for the standard image. But it has no login at all. The browser went straight to the editor, API calls worked without credentials, and the OCR endpoint answered 403 This endpoint is disabled. Only run it on a network you trust. The docs use latest in their examples, but latest moves with every release. Pin a version tag instead. All three tags above exist on docker.stirlingpdf.com, Docker Hub and ghcr.io. Docker Compose setup The compose file This is the compose file from the install docs with three changes. The tag is pinned to the version I tested, the port listens on 127.0.0.1 only, and the first admin account comes from a .env file. Create the folder and the .env first: mkdir -p ~/stirling-pdf/stirling-data/tessdata && cd ~/stirling-pdf umask 077 cat > .env <<EOF SECURITY_INITIALLOGIN_USERNAME=pdfadmin SECURITY_INITIALLOGIN_PASSWORD=$(openssl rand -base64 18 | tr -d '/+=') EOF umask 022 cat .env umask 077 makes .env readable only by you. Save the generated password in your password manager. Then save this as compose.yaml: services: stirling-pdf: image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:3.2.0 container_name: stirling-pdf ports: - "127.0.0.1:8080:8080" environment: SECURITY_INITIALLOGIN_USERNAME: ${SECURITY_INITIALLOGIN_USERNAME} SECURITY_INITIALLOGIN_PASSWORD: ${SECURITY_INITIALLOGIN_PASSWORD} volumes: - ./stirling-data/configs:/configs - ./stirling-data/logs:/logs - ./stirling-data/customFiles:/customFiles - ./stirling-data/pipeline:/pipeline - ./stirling-data/storage:/storage - ./stirling-data/tessdata:/usr/share/tessdata mem_limit: 4g restart: unless-stopped Keep mem_limit. The image sizes the Java heap from it. My container logged Detected container memory: 4096MB and let Java use up to 70% of that. Without a limit, the server FAQ says the heap is sized from the host's RAM, which is why people see Stirling-PDF use a lot of memory while idle. The mkdir creates the tessdata folder in advance on purpose. When I let Docker create it, the folder belonged to root, so I needed sudo to copy language files in later. The other folders ended up owned by UID 1000, the user the container runs as. If your files belong to a different user, the install docs say to set PUID and PGID. First start docker compose pull docker compose up -d curl -s http://127.0.0.1:8080/api/v1/info/status The pull took 17 seconds on my server. About 19 seconds after up -d, the status endpoint answered: {"version":"3.2.0","status":"UP"} That endpoint needs no login, so it's a good target for an Uptime Kuma HTTP monitor. Port 8080 now only answers on the server itself. To use the web UI from your computer, open an SSH tunnel with ssh -L 8080:127.0.0.1:8080 you@your-server and browse to http://localhost:8080, or put a reverse proxy in front (see below). Lock down the default login What a fresh install shows Login is on by default in the standard image. When I started a fresh install without the .env variables, the log said Default admin user created: admin, and the login page showed this to every visitor: !Stirling-PDF 3.2.0 login page with username and password fields and a Login button. Below them, a box titled Default Login Credentials shows Username: admin and Password: stirling, with the note Please change your password after logging in for the first time. The page prints the password, so anyone who reaches it before you change it can log in as admin. Upstream's compose file publishes '8080:8080' on every interface, and Docker's published ports skip ufw rules, so a host firewall won't save you. That's the reason for the 127.0.0.1 binding. Logging in as admin with stirling first asked whether to send anonymous usage data, then forced a new password: !Stirling-PDF Set Your Password dialog with the message For security reasons, you must change your password on your first login, the line Logged in as: admin, fields for New Password with a minimum of 8 characters and Confirm New Password, and a Change Password button. After the change, Stirling-PDF logged me out, and the credentials box was gone from the login page. The sign-in docs add that an account locks for 120 minutes after 5 failed attempts. Set your own first admin The .env approach above skips all of that. On a fresh 3.1.0 install with SECURITY_INITIALLOGIN_USERNAME=pdfadmin, the log said Admin user created: pdfadmin. The login page had no credentials box, logging in went straight to the editor with no password prompt, and admin with stirling got a 401. The docs say both variables are ignored once the database has users, so changing them later does nothing. Change the password in the app instead. Don't turn login off unless only you can reach the server. The server overview puts it plainly for SECURITY_ENABLELOGIN=false: "Anyone who can reach the server can then use it." With login on, the API is protected too. A POST to /api/v1/misc/ocr-pdf without credentials got a 401. The API description at /v1/api-docs and the Swagger page answered 200 without login, but they only describe the endpoints. Add OCR languages What's bundled, and what a missing language looks like The standard image ships Tesseract data for Simplified Chinese, English, French, German and Portuguese. tesseract --list-langs in my container showed chi_sim, deu, eng, fra, osd and por. The ultra-lite image has no OCR. I generated an image-only scan of an English invoice and OCR'd it through the API in 2.2 seconds. The invoice number, date and amounts came back right. Then I tried a scanned Spanish letter. Asking for Spanish before it was installed got an error: HTTP 400 {"detail":"Invalid OCR languages format: none of the selected languages are valid", ... "errorCode":"E041"} Per the docs, the web UI only lists installed languages. Running the Spanish letter with English worked, but every accent was gone, and "señora Núñez" became "sefiora Niifiez". Add a language with the tessdata volume The OCR languages docs describe two ways. The in-app download under Settings, Server, Advanced puts files inside the container, so they disappear when the container is recreated. The volume survives that. Download the .traineddata file from tesseract-ocr/tessdata (larger, more accurate) or tessdata_fast (smaller, faster) and restart: cd ~/stirling-pdf curl -fL -o stirling-data/tessdata/spa.traineddata \ https://github.com/tesseract-ocr/tessdata/raw/main/spa.traineddata docker compose restart docker exec stirling-pdf tesseract --list-langs The file was 18 MB. At startup, the log said Found user-mounted tessdata in /usr/share/tessdata, copying to /usr/share/tesseract-ocr/5/tessdata, and spa joined the list. Spanish then showed up in the OCR tool: !Stirling-PDF OCR / Cleanup scans tool with the scanned Spanish letter carta-scan-es.pdf open in the viewer. The settings panel shows OCR Mode Auto (skip text layers) and an open Languages list with Chinese, English (checked), French, German, Portuguese and Spanish, plus a link to the setup guide for additional languages. Here's what the two runs stored for the same scan: !Terminal capture of the OCR text Stirling-PDF 3.2.0 produced for the same scanned Spanish letter. With languages=eng the lines read Ayuntamiento de Malaga, Notificacion de pago, Estimada sefiora Niifiez, recibo del afio and Oficina de Recaudacion. With languages=spa after adding spa.traineddata they read Málaga, Notificación, señora Núñez, año and Recaudación, with a few remaining errors such as infofrnamos, esfá and pendiehte. With spa, the accents and the ñ came out right, but three words were still wrong. OCR on a noisy scan isn't perfect in any language, so check important documents. If you want every scan OCR'd and filed automatically, Paperless-ngx uses the same OCRmyPDF and Tesseract tools on each document it imports. Stirling-PDF fits one-off jobs better. The volume path is the most common mistake. In a GitHub issue where a user had mounted language files at /data/tessdata, maintainer Frooodle answered: "Your path is wrong, all our docs show it as /usr/share/tessdata". The docs add that files already in Tesseract's folder aren't replaced, and that eng.traineddata must stay, because English is always needed. Reverse proxy, updates and backups Big uploads behind nginx For HTTPS, put Stirling-PDF behind a reverse proxy that forwards to 127.0.0.1:8080. Nginx Proxy Manager does that with a free certificate, and the Umami guide shows the same 127.0.0.1 pattern behind Caddy. With nginx, check the upload size first. nginx accepts 1 MB request bodies by default, and PDFs are often bigger. I sent a 2.6 MB four-page scan through nginx 1.30.6 with a plain proxy_pass and got 413 Request Entity Too Large. The nginx log said client intended to send too large body: 2602334 bytes. The production checklist has the settings that fixed it: server { # listen 443 ssl, server_name and certificates as usual client_max_body_size 2000M; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_buffering off; proxy_read_timeout 1800s; proxy_send_timeout 1800s; } } The same request then returned 200 in 2.2 seconds with all four pages OCR'd. 2000 MB matches Stirling-PDF's own default upload limit. The long timeouts and proxy_buffering off are for slow jobs. Nginx Proxy Manager already sets client_max_body_size 2000m in the main nginx.conf of its 2.16.0 image, so this limit mostly bites hand-written nginx configs. The checklist also says to set SYSTEM_FRONTENDURL to your public address, because invite emails and share links use it. Update and back up Back up before you update. The production checklist names the folders. configs holds settings, keys and the built-in database, which also writes a nightly backup to configs/backup/db/. storage holds files users saved on the server. A tarball is enough: cd ~/stirling-pdf sudo tar czf stirling-backup-$(date +%F).tgz stirling-data/configs stirling-data/storage Then change the tag and recreate the container. I tested this on a second install that started on 3.1.0 with its own admin account: sed -i 's/stirling-pdf:3.1.0/stirling-pdf:3.2.0/' compose.yaml docker compose pull docker compose up -d curl -s http://127.0.0.1:8080/api/v1/info/status 22 seconds later the status said 3.2.0, and the admin from the first start could still log in. Read the release notes before each bump. Releases come fast: 3.0.0 came out on September 24, 2026, 3.1.0 on October 5 and 3.2.0 on October 15. Coming from V1, read the upgrade guide the install docs link to first. If you'd rather deploy from a dashboard, the same compose file works as a Docker Compose resource in Coolify. FAQ Login and access What is the default username and password for Stirling-PDF? admin and stirling. A fresh standard image creates that account and shows it on the login page until you change the password, which the first login forces. To skip the default account, set SECURITY_INITIALLOGIN_USERNAME and SECURITY_INITIALLOGIN_PASSWORD before the first start. They only apply while the user database is empty. Can I run Stirling-PDF without a login? Yes. Set SECURITY_ENABLELOGIN=false, or use the ultra-lite image, which has no login at all. Then anyone who can reach the port can use every tool and the API, as my ultra-lite test showed. Keep it on 127.0.0.1 or a private network if you do this. Is Stirling-PDF free to self-host? Yes. The repository is MIT licensed, with some folders, such as the proprietary and SaaS code, under their own licenses. Per the plans page, a self-hosted server runs on the Free plan, with all PDF tools, the REST API and OAuth2 single sign-on, for up to 5 users. Team and Enterprise add more users and features like an external database. OCR and performance Why doesn't my OCR language show up in Stirling-PDF? The .traineddata file isn't where the app looks. Mount your folder at /usr/share/tessdata inside the container, restart, and check with docker exec stirling-pdf tesseract --list-langs. If the API returns Invalid OCR languages format, the language you asked for isn't installed. Why does Stirling-PDF use so much RAM? Java sizes its heap from the memory it sees. Set mem_limit in compose or --memory with docker run. With a 4 GB limit, my standard container used between 795 MiB and 1.08 GiB while idle. If that's too much and you don't need OCR or conversions, ultra-lite used 357 MiB. Why do large uploads fail behind a reverse proxy? The proxy is rejecting the upload. nginx allows 1 MB by default and returned 413 Request Entity Too Large for my 2.6 MB scan. Set client_max_body_size 2000M in the server block and reload nginx. Nginx Proxy Manager already allows 2000 MB. --- Originally published on DeployLark: Stirling-PDF Docker Setup: Login, OCR Languages and Updates

  2. Nginx Proxy Manager with Docker: Proxy Hosts and Free SSL --- title: "Nginx Proxy Manager with Docker: Proxy Hosts and Free SSL" published: true description: "Install Nginx Proxy Manager 2.16 with Docker Compose, add proxy hosts, request Let's Encrypt SSL (HTTP-01 or Cloudflare DNS) and fix common SSL errors." tags: selfhosted, docker, nginx, ssl canonical_url: deploylark.com --- Once you run more than one self-hosted app, you hit the same problem every time. Each app wants its own port, and none of them has HTTPS. Nginx Proxy Manager (NPM) fixes both. It sits in front of your apps, answers on ports 80 and 443, sends each domain to the right container, and gets free Let's Encrypt certificates for you, all from a web UI. It's a popular choice. On October 14, 2026, the project's GitHub repository had about 34,400 stars, and the jc21/nginx-proxy-manager image had been pulled more than 285 million times from Docker Hub. This guide covers a clean install, the first-run setup (it changed in recent versions, so a lot of older guides are wrong), your first proxy host, both ways to get a certificate, and the errors you're most likely to hit. TL;DR: - Run jc21/nginx-proxy-manager:2.16.0 with ports 80 and 443 public, bind the admin UI to 127.0.0.1:81, and keep both the /data and /etc/letsencrypt volumes. - There's no admin@example.com / changeme login on 2.16. You create the admin account on the first-run screen, so use a real email, because NPM also uses it for Let's Encrypt. - Put NPM and your apps on one shared Docker network and forward to the service name and the container's internal port. localhost there means NPM itself and gives you a 502. - Request the certificate in the SSL tab. HTTP-01 needs DNS pointing at the server and port 80 open. DNS-01 with a scoped Cloudflare token also covers wildcards. Install Nginx Proxy Manager with Docker Compose Before you start Without a reverse proxy, you reach apps like this: http://203.0.113.10:3001, http://203.0.113.10:8080, and so on. That's one open port per app and no TLS. With a reverse proxy, only ports 80 and 443 are open. A request for https://status.example.com reaches NPM. NPM reads the hostname and passes the request to the container that serves it (for example uptime-kuma:3001) over the internal Docker network. NPM handles HTTPS, so the app behind it doesn't need to know anything about certificates. !Diagram of the setup in this guide. A browser request for https://status.example.com reaches Nginx Proxy Manager on ports 80 and 443. NPM picks the app by hostname and forwards over the proxy Docker network to uptime-kuma:3001 or n8n:5678. The admin UI listens on 127.0.0.1:81 only and is reached through an SSH tunnel. You need: - A Linux server with Docker and the Docker Compose plugin. - A domain, with an A record for each hostname (or a wildcard *.example.com) pointing at the server's public IP. - Ports 80 and 443 reachable from the internet (cloud firewall, router port forwarding). Port 80 matters even if you only want HTTPS, because Let's Encrypt's HTTP-01 check only works on port 80. - Nothing else already listening on 80, 443 or 81 on the host. The compose file and first start Create a folder and a compose file. This is the official quick-setup file with three changes. The image is pinned to the current release, there's a timezone you'll want to change, and the admin port is bound to 127.0.0.1: mkdir -p ~/npm && cd ~/npm nano compose.yaml services: app: image: 'jc21/nginx-proxy-manager:2.16.0' restart: unless-stopped ports: - '80:80' # public HTTP - '443:443' # public HTTPS - '127.0.0.1:81:81' # admin UI, only reachable from the server itself environment: TZ: "Europe/London" # change to your timezone # DISABLE_IPV6: 'true' # uncomment if your host has no IPv6 volumes: - ./data:/data - ./letsencrypt:/etc/letsencrypt Start it: docker compose up -d docker compose logs -f app The first start runs database migrations and takes a little while. When you see Backend PID ... listening on port 3000, it's ready. Check from the server itself, since port 81 only answers there now: curl -s http://127.0.0.1:81/api/ On a fresh install that returned: {"status":"OK","setup":false,"version":{"major":2,"minor":16,"revision":0}} "setup":false means no admin account exists yet. That's the next step. Why bind port 81 to localhost? It's your admin panel, and upstream's file publishes '81:81' on every interface. A host firewall won't save you here. Docker's firewall docs say that when you publish a container's ports, "traffic to and from that container gets diverted before it goes through the ufw firewall settings." On my test box, with the 127.0.0.1 binding, the API answered on 127.0.0.1 but not on the server's network address. To use the admin UI from your laptop, open an SSH tunnel: ssh -L 8181:127.0.0.1:81 you@your-server Then browse to http://localhost:8181. Once NPM works, you can also put the admin UI behind its own proxy host with an access list. A few more things I learned in testing: - Keep both volumes. When I started the image without /etc/letsencrypt mounted, it refused to start and logged ERROR: /etc/letsencrypt is not mounted! Check your docker configuration. - SQLite is the default and is fine for a single server. The full setup docs also have MariaDB, MySQL and Postgres variants. - The project doesn't publish minimum requirements. On my test box the idle container used about 110 to 130 MB of RAM. - Images are built for amd64 and arm64. The setup docs say armv7 was dropped in 2.14 and point 32-bit ARM users to the 2.13.7 tag. Create the admin account The setup screen replaced the default login Open the admin UI through the SSH tunnel at http://localhost:8181. If you kept upstream's '81:81', use http://<server-ip>:81. If you've read an older guide, you're expecting to log in with admin@example.com and changeme. That was true up to the 2.12.x releases (it's in the v2.12.6 README), but it was removed from the README in 2.13.0. On 2.16.0 there is no default account. You get a "Welcome! Get started by creating your admin account" screen with Full Name, Email address and New Password. !Nginx Proxy Manager 2.16 first-run setup screen, where you create the admin account with a full name, email address and password Use a real email address here. NPM uses your account email as the Let's Encrypt account email. In my test the certificate request ran certbot ... -m admin@lab.test ..., and Let's Encrypt rejected it with The ACME server believes admin@lab.test is an invalid email address. A made-up address here means certificate requests fail later. Scripted setup and two-factor login If you'd rather create the admin account without the setup screen (useful for scripted installs), the advanced config docs list two variables for it. Don't type the password into compose.yaml. Put it in a .env file next to it that only you can read: cat > .env <<'EOF2' INITIAL_ADMIN_EMAIL=you@example.com INITIAL_ADMIN_PASSWORD=a-long-random-password EOF2 chmod 600 .env Then reference the variables in compose.yaml: environment: INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL} INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD} After the first start, delete both lines from compose.yaml, delete .env, and run docker compose up -d. I tested this. The account was created, /api/ reported "setup":true, and after removing the variables and recreating the container I could still log in with the same password. Change the password in the UI if it was ever stored anywhere else. NPM has supported TOTP two-factor authentication since 2.13.6. The 2.16 UI includes an "Enable Two-Factor Authentication" option for your account. Turn it on, because this panel controls all your traffic. Add a proxy host Connect NPM to your apps with a shared network The easiest way to connect NPM to your apps is a shared Docker network. NPM then reaches each app by its service name, and the apps don't need to publish any ports to the host. This is the approach the NPM docs recommend: docker network create proxy Add this to the bottom of NPM's compose.yaml and to each app's compose file: networks: default: external: true name: proxy Then run docker compose up -d again in each folder. If an app's compose file already defines its own networks, attach just the web service to proxy instead. In the NPM UI, go to Hosts > Proxy Hosts > Add Proxy Host. The Details tab has: - Domain Names: status.example.com - Scheme: http. This is the protocol between NPM and the app, which is almost always plain HTTP. - Forward Hostname / IP: the container's service name, for example uptime-kuma. - Forward Port: the port the app listens on inside its container (for example 3001), not a host port. - Access List: leave it as Publicly Accessible for now. - Options: Block Common Exploits is a sensible default. Turn on Websockets Support for any app with a live UI (see below). This is the Details tab from my lab run, pointing a test hostname at a container named whoami: !Add Proxy Host dialog in Nginx Proxy Manager 2.16, Details tab: domain status.lab.test, scheme http, forward hostname whoami, forward port 80, access list Publicly Accessible, with Block Common Exploits and Websockets Support switched on Save, then test from any machine: curl -I http://status.example.com In my lab, a proxy host pointing at a container named whoami on the shared network returned that container's page, and a hostname with no proxy host got NPM's "Congratulations!" default site. You can change that default under Settings > Default Site. The saved host shows up in the Proxy Hosts list: !Nginx Proxy Manager Proxy Hosts list with one entry: source status.lab.test, destination http://whoami:80, SSL HTTP Only, access Public, status Online For a full example app behind NPM, see Uptime Kuma with Docker. Websockets, access lists and custom locations Some apps won't work at all without HTTPS. Vaultwarden's web vault is one, as the Vaultwarden HTTPS guide shows. I tested that fix with Caddy, and an NPM proxy host with a Let's Encrypt certificate should fix it the same way. Websockets Support (Details tab) matters for apps with a live UI, such as Uptime Kuma, n8n, Home Assistant or Open WebUI. They break in strange ways without it, with "connection lost" banners and dashboards that never update. The n8n guide and the Open WebUI guide show what that looks like. When I turned the toggle on, NPM added these lines to the generated nginx config: proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_http_version 1.1; If you aren't sure whether an app needs it, turn it on. It doesn't hurt plain HTTP apps. Access lists (Access Lists menu) hold HTTP basic-auth users and allow or deny IP rules. Pick one in a proxy host's Access List field. It's a cheap way to lock down admin panels that have weak or no auth of their own. NPM 2.16.0 also added per-path access lists inside the proxy host modal. Custom Locations (a tab in the proxy host) send a path such as /api to a different container or port than the rest of the host. Most single-app hosts don't need them. The Advanced tab (gear icon) takes raw nginx directives for the server block. Use it sparingly. If a snippet breaks the config, 2.16.0 keeps the failed file as .conf.err so you can see what went wrong. Get a free Let's Encrypt certificate Edit the proxy host and open the SSL tab. Under SSL Certificate, pick "Request a new Certificate (with Let's Encrypt)". The other toggles are: - Force SSL redirects HTTP to HTTPS. Turn it on once the certificate works. - HTTP/2 Support is safe to enable. - HSTS Enabled and HSTS Sub-domains tell browsers to use only HTTPS for this host (and its subdomains) for a long time. Leave these off until you're sure HTTPS works everywhere, because they're hard to undo. !SSL tab of the Add Proxy Host dialog with the SSL Certificate dropdown open, offering None (this host will not use HTTPS) or Request a new Certificate with Let's Encrypt Then pick a challenge type. HTTP-01 (the default) Per Let's Encrypt's docs, HTTP-01 asks for a token at http://<your-domain>/.well-known/acme-challenge/... and checks that your server returns it. NPM serves that path itself, from a letsencrypt-acme-challenge folder in its data volume. The challenge can only succeed when DNS for the hostname already points at this server and port 80 is reachable from the internet. On Oct 14, 2026, I verified this domain and HTTP-01 path by getting a real Let's Encrypt certificate over HTTP-01 on a public Debian 13 server running Caddy, while NPM's own certificate request screen follows NPM's documentation. HTTP-01 can't issue wildcard certificates, and it fails if your ISP blocks port 80. NPM 2.16's code also includes a reachability check for HTTP-01. It writes a test file and asks an outside service (site24x7.com) to fetch it from your domain. Run it before you request a real certificate, because failed attempts count toward rate limits (covered below). DNS-01 with Cloudflare With DNS-01, according to Let's Encrypt's docs, certbot proves you control the domain by creating a TXT record at _acme-challenge.<your-domain>. Use it when: - you want a wildcard certificate (*.example.com), - the server isn't reachable on port 80 (a home lab behind CGNAT, or an internal-only service), or - you have several servers. In the SSL tab, turn on Use DNS Challenge, choose your DNS Provider, and paste the credentials. NPM 2.16.0 ships certbot plugins for a long list of providers, Cloudflare included. For Cloudflare, NPM's template is: # Cloudflare API token dns_cloudflare_api_token=your-cloudflare-api-token Create an API token (not the Global API Key) in the Cloudflare dashboard. The certbot-dns-cloudflare docs say it needs Zone:DNS:Edit permission, only for the zones you need certificates for. Two cautions: - That token can change your DNS. Let's Encrypt's challenge docs warn that keeping DNS API credentials on a web server raises the stakes if the server is compromised, so scope the token as narrowly as you can. - 2.16.0 updated certbot, and the release notes warn that some DNS plugins may need dependency tweaks. If your provider's plugin errors after an upgrade, check the issue tracker before you assume your token is wrong. Fix common SSL errors Certificate requests that fail "Internal Error" when requesting a certificate This is NPM's generic message. The real reason is in the certbot log, which sits in your data volume: sudo tail -n 50 ~/npm/data/logs/letsencrypt.log # the data folder is owned by root docker compose logs app | grep -i -A5 "SSL\|certbot" The usual causes are DNS not pointing at the server yet, port 80 blocked, or an invalid account email (see above). Challenge fails with "Timeout", "Connection refused" or the wrong IP Let's Encrypt couldn't fetch the token. Check these from outside your network (mobile data works): dig +short status.example.com # should print your server's public IP curl -I http://status.example.com # should answer from NPM, not time out If the IP is wrong, fix DNS and wait for it to update. If you get a timeout, open port 80 in the cloud firewall or router. If you use Cloudflare's proxy (orange cloud) and HTTP-01 keeps failing, switch to the DNS challenge, which doesn't depend on the HTTP path. Rate limited Retrying over and over burns Let's Encrypt's rate limits. The ones you're most likely to hit while troubleshooting: - 5 failed validations per hostname, per account, per hour. - 5 certificates for the exact same set of hostnames every 7 days. Reinstalling and requesting again is the classic way to hit this one. - 50 certificates per registered domain every 7 days. Revoking certificates doesn't reset these limits. Fix the cause first, test it, then request once. Errors after the certificate is issued ERR_TOO_MANY_REDIRECTS behind Cloudflare If Cloudflare's SSL/TLS mode is Flexible, Cloudflare talks to your server over plain HTTP. With Force SSL on, NPM redirects that to HTTPS, and Cloudflare requests HTTP again, in a loop. Cloudflare's fix is to set the mode to Full or higher, which needs a valid certificate on the origin (NPM's Let's Encrypt certificate works here), or else to stop redirecting at the origin. 502 Bad Gateway after a "successful" setup The certificate is fine, but NPM can't reach the app. The most common mistake is putting localhost or 127.0.0.1 in Forward Hostname. Inside the NPM container, that address means NPM itself. In my lab, a proxy host pointed at 127.0.0.1:8080 returned 502, and the host's error log said: connect() failed (111: Connection refused) while connecting to upstream ... upstream: "http://127.0.0.1:8080/" Use the app's service name on the shared Docker network, with the container's internal port. If the app runs directly on the host instead of in Docker, use the host's LAN IP. Per-host logs are in ~/npm/data/logs/proxy-host-<id>_error.log. FAQ Logging in and updating Why doesn't admin@example.com / changeme work? Because 2.16 has no default account. Since 2.13.0, a fresh install shows a setup screen where you create the admin account yourself. How do I update Nginx Proxy Manager? The upgrading page says to run docker compose pull and docker compose up -d, and adds: "This project will automatically update any databases or other requirements so you don't have to follow any crazy instructions." Some releases do add migrations, so I'd back up first. With a pinned tag, change it in compose.yaml, then: cd ~/npm docker compose stop # don't copy the SQLite file while it's in use sudo cp -a data data.bak-$(date +%F) docker compose pull && docker compose up -d Read the release notes before major bumps. 2.15.0 and 2.16.0 both updated certbot and warned that DNS plugins may need changes. Version 2.13.3 added a migration and warned that you can't easily downgrade after it. Why are my settings gone after Docker restarts? NPM keeps its database and config in /data and its certificates in /etc/letsencrypt. If those aren't mounted to folders or volumes on the host, recreating the container starts from scratch. Use both volume lines from the compose file above. The official image won't even start without /etc/letsencrypt mounted. Certificates and errors How do I get a wildcard certificate? Use the DNS challenge. HTTP-01 can't issue wildcards. Request a certificate for *.example.com (and example.com if you want the bare domain too), turn on Use DNS Challenge, pick your provider and paste a scoped API token, as in the Cloudflare example above. Why do I get 502 Bad Gateway? NPM can't reach the app at the Forward Hostname and port you gave it. Don't use localhost or 127.0.0.1, which mean the NPM container itself. Put NPM and the app on the same Docker network and use the service name and the container's internal port. How do I protect the admin panel on port 81? Don't publish it to the internet. Port 81 serves the login form over plain HTTP. The compose file above binds it to 127.0.0.1, and on my test box it then answered only on the server itself. Reach it through an SSH tunnel (ssh -L 8181:127.0.0.1:81 you@your-server). A reply in issue #481 suggests another option: a proxy host such as npm.example.com that forwards to 127.0.0.1 port 81 with SSL, after which you remove the port 81 mapping from the compose file. If you go that way, I'd also add an access list to that proxy host so only your own IP addresses can reach it. Turn on two-factor login either way. --- Originally published on DeployLark: Nginx Proxy Manager with Docker: Proxy Hosts and Free SSL

    1 reaction

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

12
PackageVersionDownloads · 30dTrend · 30d
———
———
———
———
———
———
———
———
———
———
———
npm-testv0.0.1
v0.0.1
≥1.4K

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
NginxProxyManager/nginx-proxy-manager mentions in the last 30 days