Skip to content

Introduced support for load balancing through upstream hosts and customizable Real IP header source#5413

Open
genticflowlabs wants to merge 6 commits into
NginxProxyManager:developfrom
genticflowlabs:feature/real-ip-header-and-upstream-hosts
Open

Introduced support for load balancing through upstream hosts and customizable Real IP header source#5413
genticflowlabs wants to merge 6 commits into
NginxProxyManager:developfrom
genticflowlabs:feature/real-ip-header-and-upstream-hosts

Conversation

@genticflowlabs
Copy link
Copy Markdown

@genticflowlabs genticflowlabs commented Mar 17, 2026

Upstream Hosts, Real IP Header setting, and Cloudflare compatibility

Closes #5374
Closes #156
Heavily influenced by #5184

Summary

This PR introduces two major features: Upstream Hosts for load balancing across multiple backend servers, and a configurable Real IP Header setting that fixes Access List IP whitelisting when behind Cloudflare or other CDNs.

Features

Upstream Hosts

image image image image image image

A new first-class entity for managing reusable nginx upstream groups. Upstream hosts support three load balancing methods: round-robin, least connections, and IP hash, each with configurable server weights.

  • Backend: New model (upstream_host, upstream_host_server), internal logic, REST API (/api/nginx/upstream-hosts), access control rules, schema definitions, and nginx template (upstream_host.conf)
  • Migration: Creates upstream_host and upstream_host_server tables, adds upstream_host_id foreign key to proxy_host
  • Frontend: Full CRUD UI with table listing and modal for create/edit. Server list management with host, port, and weight fields
  • Proxy Host integration: Radio button toggle (Direct / Upstream Host) replaces the old dropdown, showing only the relevant fields for each mode
  • Custom Locations: Same Direct/Upstream radio pattern with a rich react-select dropdown matching the proxy host UX
  • Proxy Host table: Displays the upstream host name when one is selected

Real IP Header Setting

image

A new global setting under Settings > Real IP Header that controls which HTTP header nginx uses for real_ip_header. This enables Access Lists to work behind Cloudflare.

  • Options: X-Real-IP (default), CF-Connecting-IP (Cloudflare), X-Forwarded-For, or a custom header name
  • How it works: The real_ip_header directive is moved from the static nginx.conf into the dynamically generated ip_ranges.conf. Changing the setting triggers config regeneration and nginx reload
  • Settings API: Schema updated to accept both default-site and real-ip-header values

Startup Config Regeneration

All nginx host configs are now deleted and regenerated from current templates on every startup. This ensures configs on disk always match the current template version after an upgrade, preventing stale configs from blocking nginx. For example, if a template change adds or removes a directive, old configs with the previous format would cause nginx to fail to start. Regenerating on startup eliminates this class of issues entirely.

Translations

All new keys are 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) with proper native-language translations.

AI Disclosure

AI (Claude) was used as a development assistant during this work. All changes have been tested on a deployed instance, however I would greatly appreciate additional QA help from the community to cover edge cases across different configurations and environments.

@nginxproxymanagerci
Copy link
Copy Markdown

Docker Image for build 6 is available on DockerHub:

nginxproxymanager/nginx-proxy-manager-dev:pr-5413

Note

Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
This is a different docker image namespace than the official image.

Warning

Changes and additions to DNS Providers require verification by at least 2 members of the community!

@DrEVILish
Copy link
Copy Markdown

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.
https://docs.nginx.com/nginx/admin-guide/load-balancer/tcp-udp-load-balancer/
Use case would be adding LB to a DNS cluster, LB'ing / Failover from a single DNS is quicker than the client failing over to secondary DNS.

Any LB should implement health checks to ensure that the upstream hosts are online and available.

@jc21
Copy link
Copy Markdown
Member

jc21 commented May 14, 2026

Code Review — Upstream Hosts + Real IP Header

Thanks 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.


🔴 Critical

1. Nginx directive injection via custom real_ip_header

  • Files: backend/schema/paths/settings/settingID/put.json, backend/internal/ip_ranges.js
  • The meta.custom field has no pattern or length validation. An admin could set it to something like X-Real-IP; proxy_set_header X-Injected yes and inject arbitrary nginx directives directly into the rendered config.
  • Fix: Add "pattern": "^[A-Za-z][A-Za-z0-9-]*$" and "maxLength": 128 to the custom header schema. Validate in ip_ranges.js before rendering.
  • The silent catch (_) {} around the DB query in ip_ranges.js is also concerning — errors are swallowed with no log warning.

🔴 High

2. real_ip_header absent when IP_RANGES_FETCH_ENABLED=false

  • The hardcoded real_ip_header X-Real-IP; was removed from nginx.conf, but ip_ranges.conf is only written when the fetch runs. On installs with fetching disabled, real_ip_header is never written to any config file, breaking real IP detection entirely.
  • Fix: Either restore a fallback in nginx.conf, or call generateConfig with empty/cached ranges during setupNginxConfigs.

3. ip_hash + weight are incompatible in nginx

  • File: backend/templates/upstream_host.conf
  • The template renders weight={{ server.weight }} regardless of load balancing method. Nginx rejects weight inside an ip_hash block with a config error, preventing reload.
  • Fix: Conditionally omit weight= when method == "ip_hash".

4. Asset caching broken for upstream hosts

  • File: backend/templates/_assets.conf
  • When an upstream host is selected, the template still renders proxy_pass $forward_scheme://$server:$port where $server is set to 127.0.0.1 by the frontend. Cached assets will be fetched from localhost rather than the upstream pool.
  • There's also a discrepancy: the static assets.conf appends $request_uri but the Liquid template does not.

5. proxy.conf breaking change (undocumented)

  • proxy_pass was removed from the shared conf.d/include/proxy.conf. Any custom server configs that include this file and rely on it for proxy_pass will silently stop forwarding requests after upgrading.

🟡 Medium

6. Startup wipes all configs before regeneration (outage risk)

  • File: backend/setup.js, setupNginxConfigs
  • All .conf files are deleted before regeneration begins. If generateConfig fails mid-loop (template error, DB issue mid-restart), nginx reloads with no configs for that host type — causing an outage window on every container restart.
  • Fix: Write to temp filenames, test with nginx -t, then atomically swap. Or test before deleting anything.

7. Double nginx reload on upstream host update

  • File: backend/internal/upstream-host.js, update()
  • configure() already calls reload() internally; the code then calls reload() again explicitly. The bulkGenerateConfigs call for proxy hosts also bypasses the configure() lifecycle, so errors won't update nginx_err on those records.

8. user-object.jsonadditionalProperties: false removed

  • Removed to allow the upstream_hosts permission field, but this weakens schema validation. The correct fix is to keep additionalProperties: false and explicitly list all valid properties including upstream_hosts.

9. No format validation on server.host field

  • A malformed host value like 10.0.0.1; server 127.0.0.1 injected into the upstream template produces invalid nginx config. The JSON schema should add "pattern": "^[a-zA-Z0-9._\\-\\[\\]:]+$" to the host field.

10. React state mutation during render in LoadBalancingFields.tsx

  • setValues([blankItem]) is called directly in the component body when values.length === 0 — a React anti-pattern that can cause infinite re-render loops in strict mode.
  • Fix: Wrap in useEffect(() => { if (values.length === 0) setValues([blankItem]); }, []).

11. Dual state divergence in UpstreamHostModal.tsx

  • LoadBalancingFields manages its own useState alongside Formik's values. If Formik resets the form, the component's internal state won't reset, causing them to diverge.

🟢 Low

12. Slovak locale replaced with Czech

  • frontend/src/locale/src/sk.json — new load-balancing strings appear to be Czech translations, not Slovak.

13. Garbled translations in bg.json and ko.json

  • "load-balancing.add-server" shows ???????? / ?? ?? — likely an encoding issue during AI-assisted translation.

14. Migration: upstream_host_id uses 0 instead of null

  • Consistent with the rest of the codebase (no FK constraints used), but using 0 as a sentinel instead of null creates semantic ambiguity.

15. Missing test coverage

  • No test that ip_hash + weight generates valid nginx syntax
  • No test for startup config regeneration path after an upgrade
  • No test for asset caching behaviour with upstream hosts

Summary

Issues #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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Respect X-Forwarded-For or X-Real-IP for IP ACLs Support upstream / load balancing

3 participants