Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 69 additions & 3 deletions backend/internal/nginx.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import fs from "node:fs";
import { lookup } from "node:dns/promises";
import { isIP } from "node:net";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import _ from "lodash";
Expand All @@ -8,6 +10,7 @@ import { debug, nginx as logger } from "../logger.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]);

const internalNginx = {
/**
Expand Down Expand Up @@ -172,6 +175,8 @@ const internalNginx = {
locationCopy.forward_path = `/${splitted.join("/")}`;
}

locationCopy.forward_host = await internalNginx.preResolveUpstreamHost(locationCopy.forward_host);

renderedLocations += await renderEngine.parseAndRender(template, locationCopy);
}
};
Expand Down Expand Up @@ -238,10 +243,14 @@ const internalNginx = {
locationsPromise = Promise.resolve();
}

const forwardHostPromise = internalNginx.preResolveUpstreamHost(host.forward_host).then((resolvedHost) => {
host.forward_host = resolvedHost;
});

// Set the IPv6 setting for the host
host.ipv6 = internalNginx.ipv6Enabled();

locationsPromise.then(() => {
Promise.all([locationsPromise, forwardHostPromise]).then(() => {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
Expand All @@ -258,8 +267,8 @@ const internalNginx = {
reject(new errs.ConfigurationError(err.message));
});
});
});
},
});
},

/**
* This generates a temporary nginx config listening on port 80 for the domain names listed
Expand Down Expand Up @@ -421,6 +430,63 @@ const internalNginx = {
*/
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im),

/**
* @returns {boolean}
*/
preResolveUpstreamHostEnabled: () => {
if (typeof process.env.NPM_PRE_RESOLVE_UPSTREAM_HOSTS === "undefined") {
return false;
}
return TRUE_ENV_VALUES.has(String(process.env.NPM_PRE_RESOLVE_UPSTREAM_HOSTS).trim().toLowerCase());
},

/**
* @param {String} host
* @returns {boolean}
*/
canPreResolveUpstreamHost: (host) => {
if (typeof host !== "string") {
return false;
}
const candidate = host.trim();
if (candidate === "") {
return false;
}
if (candidate.includes("$") || candidate.includes("/") || candidate.includes(":")) {
return false;
}
return isIP(candidate) === 0;
},

/**
* @param {String} host
* @returns {Promise<String>}
*/
preResolveUpstreamHost: async (host) => {
if (!internalNginx.preResolveUpstreamHostEnabled()) {
return host;
}
if (typeof host !== "string") {
return host;
}
const candidate = host.trim();
if (!internalNginx.canPreResolveUpstreamHost(candidate)) {
return host;
}

try {
const resolved = await lookup(candidate, { family: 0, all: false, verbatim: false });
if (resolved?.address) {
debug(logger, `Pre-resolved upstream host "${candidate}" to "${resolved.address}"`);
return resolved.address;
}
} catch (err) {
debug(logger, `Could not pre-resolve upstream host "${candidate}": ${err.message}`);
}

return host;
},

/**
* @returns {boolean}
*/
Expand Down
24 changes: 24 additions & 0 deletions docs/src/advanced-config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,30 @@ By default, NPM fetches IP ranges from CloudFront and Cloudflare during applicat
IP_RANGES_FETCH_ENABLED: 'false'
```

## Pre-resolving Upstream Hostnames

By default, proxy upstream hostnames are resolved by Nginx's configured DNS resolver.
In some Docker setups, hostnames such as `host.docker.internal` may only be available
through the container's system resolver path (for example `extra_hosts`) and may not be
resolvable by Nginx runtime DNS resolution.

To enable optional pre-resolution in NPM while generating proxy configs:

```yml
environment:
NPM_PRE_RESOLVE_UPSTREAM_HOSTS: 'true'
```

When enabled, NPM attempts to resolve eligible upstream hostnames via the system resolver
before writing Nginx config and stores the resolved IP in generated upstream targets.
If resolution fails, NPM keeps the original hostname.

Notes:

- Default is disabled (`false`) to preserve existing behavior.
- Resolution happens when configs are generated/re-generated, not per request at runtime.
- If upstream IPs change, regenerate or reload the related proxy host config.

## Custom Nginx Configurations

If you are a more advanced user, you might be itching for extra Nginx customizability.
Expand Down
Loading