Introduced support for load balancing through upstream hosts and customizable Real IP header source#5413
Conversation
|
I might be missing something, but does this only add LB to HTTP and HTTPS connections? Would this be able to LB other ports / TCP and UDP streams i.e. Any LB should implement health checks to ensure that the upstream hosts are online and available. |
Code Review — Upstream Hosts + Real IP HeaderThanks for this substantial contribution. The overall structure is solid and the test coverage is appreciated. A few issues need addressing before this can merge, ranging from blockers to low-priority fixes. 🔴 Critical1. Nginx directive injection via custom
🔴 High2.
3.
4. Asset caching broken for upstream hosts
5.
🟡 Medium6. Startup wipes all configs before regeneration (outage risk)
7. Double nginx reload on upstream host update
8.
9. No format validation on
10. React state mutation during render in
11. Dual state divergence in
🟢 Low12. Slovak locale replaced with Czech
13. Garbled translations in
14. Migration:
15. Missing test coverage
SummaryIssues #1 (directive injection), #3 (ip_hash/weight nginx incompatibility), and #6 (startup outage risk) are blockers. Issue #2 (real_ip_header absent when IP range fetching is disabled) is a significant regression for a real user segment. The translation issues (#12, #13) are easy wins to address now. The underlying feature design is good and solves real problems — looking forward to seeing a revised version. |
Brings the feature branch up to date with upstream NginxProxyManager/develop so the open PR can merge cleanly. The only real conflict was in frontend/src/pages/Nginx/ProxyHosts/Table.tsx where upstream added a basic forwardHost cell renderer while this branch had already replaced it with the location-aware DestinationCell — kept ours. The auto-merger also undid the vite-tsconfig-paths plugin fix (because upstream introduced a duplicate-broken `resolve.tsconfigPaths: true` option in a separate commit); restored our working version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The file got mutated locally (the dev compose's authentik service writes to its postgres init fixture during boot). Restore it to upstream so the PR diff doesn't include a noise change to a binary CI fixture.
Drop the vite-tsconfig-paths plugin import — it was added locally to fix local production builds but was never declared in package.json, which broke CI on a fresh install. Upstream's CI doesn't run vite build (only vitest), and vitest happens to not exercise the src/* alias, so its no-op `resolve.tsconfigPaths: true` is fine for them and now for us too. The production build will hit the same path-resolution issue if anyone tries to run `vite build` directly, but that's a separate concern from this PR; matching upstream behavior so CI is green is the priority here.
Upstream's new test (cbcbd95) asserts en-US → EN, de-DE → DE, etc. — i.e. flag follows the language, not the region. Our previous version returned the region whenever one was present (US for en-US), which we had introduced to make pt-BR show the Brazilian flag. Rework to keep upstream's language-first behavior and add an explicit exception for pt-BR (the only locale we register with a non-default region). en-US, de-DE, fr-FR, ga-IE all fall through to the language path, matching the test expectations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The configureServer plugin was spawning yarn locale-compile via execFile (async) and not waiting for it. On CI's slower filesystem the truncate phase of the second compile-folder run raced with vitest's JSON imports of the same files and produced "EOF while parsing a value" — the test file failed to load before any tests could run. Switch to execFileSync so the locale rewrite always completes before imports start. Behaviour is identical for `yarn dev`; just blocks briefly on startup instead of running in the background.
The 201 example included id/created_on/modified_on/user_id inside the permissions object, but the API explicitly omits those four keys from the response (backend/internal/user.js omissions list). Adding additionalProperties: false on the permissions sub-schema (PR review fix-up) made the stale example fail Vacuum's oas3-valid-schema-example rule in the Cypress Swagger Schema Linting test. Drop the DB-only fields from the example so it matches the actual response shape; add the new upstream_hosts permission to the example while we're at it.
In LocationsFields the targetType for each location row was derived purely from upstreamHostId (> 0 → "upstream", else "direct"). When the user clicked the Upstream radio on a fresh location with no upstream selected yet, handleTargetTypeChange wrote upstreamHostId: 0, so the next render immediately snapped the radio back to Direct — and only the hover highlight was visible because the input toggled briefly and then reverted. Track the user's explicit choice in a per-index override map, falling back to the derivation only when no choice has been made yet. Compact the map on remove so indices line up after deletion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…time When a Custom Location targets an upstream host, the user never fills in forward_scheme/host/port for that row, so the payload had forward_host:"" on those locations. The backend schema requires forward_host minLength 1 on every location item regardless of upstream_host_id, so saving failed with 'data/locations/N/forward_host must NOT have fewer than 1 characters'. Mirror the same defaulting we already do for the proxy host's top-level forward fields: at submit time, locations with upstreamHostId > 0 get forward_scheme "http", forward_host "127.0.0.1", forward_port 80. These placeholders never reach nginx — _location.conf switches to the upstream block (proxy_pass upstream_host_<id>) when upstream_host_id > 0 — so the actual routing is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DestinationCell previously had two bugs in the same expression: 1. When the proxy host carried a proxy-level upstream_host_id, the cell early-returned with just the upstream button — never reaching the block that renders custom locations. So any host using both a proxy- level upstream and per-location overrides looked single-target. 2. Even when the early return didn't fire, the per-location render only kept locations whose upstream_host_id > 0. Direct-mode custom locations (forward_host:port) were never shown. Rewrite so the cell always shows the proxy-level destination on the first line (upstream button OR scheme://host:port) and every custom location underneath as `<path> → <upstream-button | scheme://host:port>`. If `/` is itself a custom location the proxy-level line is suppressed, since every request matches an override and the proxy-level forward is unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a proxy host has custom locations, show the proxy-level (default) destination as '/ → <dest>' so it reads uniformly alongside the other `<path> → <dest>` rows. When the host has no custom locations, keep the old tight rendering (just the destination, no '/' prefix). The '/' row is still suppressed when the user has explicitly defined a '/' custom location — that override fully replaces the default, so showing both would be misleading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Docker Image for build 17 is available on DockerHub: Note Ensure you backup your NPM instance before testing this image! Especially if there are database changes. Warning Changes and additions to DNS Providers require verification by at least 2 members of the community! |

Closes #5374
Closes #156
Heavily influenced by #5184
Why
This PR addresses two long-standing requests:
real_ip_headerwas hardcoded toX-Real-IP, so the IP whitelist on an Access List sees Cloudflare's IPs instead of the real client. Users had to manually edit nginx configs or give up on whitelisting.To get there it also introduces a small piece of operational hardening: nginx host configs are regenerated from templates on every startup, so template changes ship cleanly across upgrades without leaving stale configs that break nginx on boot.
Upstream Hosts
First-class entity for reusable nginx
upstreamgroups with three load-balancing methods (round-robin, least connections, ip_hash) and per-server weights. Includes:upstream_host,upstream_host_server, plusupstream_host_idFK onproxy_host/api/nginx/upstream-hosts, access-control rules, schema definitions, audit-log entriesupstream_host.conf); proxy hosts can select an upstream group at the host level or per locationReal IP Header setting
New global setting under Settings → Real IP Header that controls the nginx
real_ip_headerdirective:X-Real-IP(default),CF-Connecting-IP,X-Forwarded-For, or a custom header name (pattern-validated to block directive injection)nginx.confinto the dynamically generatedip_ranges.conf; changing the setting regenerates and reloadsdefault-siteandreal-ip-headervaluesStartup config regeneration
On every backend startup, host configs are regenerated in place from current templates. Regeneration is safe (overwrite in place; if a template fails, the previous good config stays on disk), and orphan cleanup runs only for hosts no longer in the DB. This lets the template format evolve across releases without breaking nginx on the user's next container restart.
Translations
New keys translated across all 22 supported locales (bg, cs, de, es, et, fr, ga, hu, id, it, ja, ko, nl, no, pl, pt, pt_br, ru, sk, tr, vi, zh).
Type of Change
AI Usage
AI (Claude) was used as a development assistant for the implementation and for an independent QA pass against the prior round of review comments. All changes have been tested on a deployed instance; additional community QA across different configurations and environments would be welcome.