fix(deploy): reject loopback API_PROXY_TARGET in production builds

Prevents baking self-proxy /api and /chat rewrites that pin a CPU core inside Docker.
This commit is contained in:
alisaza 2026-09-08 14:06:20 +03:30
parent 5404974c32
commit 8d2e19ac7d
3 changed files with 48 additions and 2 deletions

View File

@ -1,4 +1,7 @@
NEXT_PUBLIC_SITE_URL=https://backoffice.ghabilee.ir NEXT_PUBLIC_SITE_URL=https://backoffice.ghabilee.ir
NEXT_PUBLIC_API_URL= NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_FILE_SERVER_URL= NEXT_PUBLIC_FILE_SERVER_URL=
# Dev-only same-origin proxy to Nest. Leave empty in Docker/production.
# Never use http://127.0.0.1:3000 / localhost here in a container — that is this
# Next process and bakes a self-proxy CPU loop into the image at build time.
API_PROXY_TARGET= API_PROXY_TARGET=

View File

@ -14,8 +14,36 @@ const isProduction = process.env.NODE_ENV === 'production'
* local HTTP). Rewriting local HTTP Next to production Nest can therefore * local HTTP). Rewriting local HTTP Next to production Nest can therefore
* emit a Secure `__Host-` cookie the browser may reject. Prefer local Nest * emit a Secure `__Host-` cookie the browser may reject. Prefer local Nest
* when testing login/refresh. See docs/reference/environment-variables.md. * when testing login/refresh. See docs/reference/environment-variables.md.
*
* Never point API_PROXY_TARGET at loopback in Docker/production: inside the
* container 127.0.0.1:3000 is this Next process, so /api and /chat rewrites
* become a self-proxy CPU loop. Leave empty when the browser uses a public
* NEXT_PUBLIC_API_URL, or use the backend service hostname on a shared network.
*/ */
const apiProxyTarget = (process.env.API_PROXY_TARGET || '').replace(/\/+$/, '') const resolveApiProxyTarget = (raw) => {
const trimmed = (raw || '').trim().replace(/\/+$/, '')
if (!trimmed) return ''
let hostname = ''
try {
hostname = new URL(trimmed).hostname
} catch {
console.warn(`[next.config] Ignoring invalid API_PROXY_TARGET=${JSON.stringify(raw)}`)
return ''
}
const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
if (isLoopback) {
console.warn(
`[next.config] Ignoring loopback API_PROXY_TARGET=${trimmed} (points at this Next process in Docker and causes a CPU loop)`
)
return ''
}
return trimmed
}
const apiProxyTarget = resolveApiProxyTarget(process.env.API_PROXY_TARGET)
/** Path prefix for reverse-proxy deploys (e.g. `/ghabilee`). Empty = site root. */ /** Path prefix for reverse-proxy deploys (e.g. `/ghabilee`). Empty = site root. */
const rawBasePath = (process.env.NEXT_PUBLIC_BASE_PATH || '').trim() const rawBasePath = (process.env.NEXT_PUBLIC_BASE_PATH || '').trim()

View File

@ -25,6 +25,21 @@ env_or_empty() {
printf '%s' "${!key-}" printf '%s' "${!key-}"
} }
# Loopback targets bake self-proxy rewrites into the image and peg a CPU core in Docker.
sanitize_api_proxy_target() {
local target
target="$(env_or_empty API_PROXY_TARGET)"
case "$target" in
*127.0.0.1*|*localhost*|*'::1'*)
echo "Warning: refusing loopback API_PROXY_TARGET for production image build" >&2
printf ''
;;
*)
printf '%s' "$target"
;;
esac
}
BUILD_ARGS=( BUILD_ARGS=(
--build-arg "NEXT_PUBLIC_API_URL=$(env_or_empty NEXT_PUBLIC_API_URL)" --build-arg "NEXT_PUBLIC_API_URL=$(env_or_empty NEXT_PUBLIC_API_URL)"
--build-arg "NEXT_PUBLIC_FILE_SERVER_URL=$(env_or_empty NEXT_PUBLIC_FILE_SERVER_URL)" --build-arg "NEXT_PUBLIC_FILE_SERVER_URL=$(env_or_empty NEXT_PUBLIC_FILE_SERVER_URL)"
@ -41,7 +56,7 @@ BUILD_ARGS=(
--build-arg "SENTRY_AUTH_TOKEN=$(env_or_empty SENTRY_AUTH_TOKEN)" --build-arg "SENTRY_AUTH_TOKEN=$(env_or_empty SENTRY_AUTH_TOKEN)"
--build-arg "SENTRY_ORG=$(env_or_empty SENTRY_ORG)" --build-arg "SENTRY_ORG=$(env_or_empty SENTRY_ORG)"
--build-arg "SENTRY_PROJECT=$(env_or_empty SENTRY_PROJECT)" --build-arg "SENTRY_PROJECT=$(env_or_empty SENTRY_PROJECT)"
--build-arg "API_PROXY_TARGET=$(env_or_empty API_PROXY_TARGET)" --build-arg "API_PROXY_TARGET=$(sanitize_api_proxy_target)"
) )
echo "Building ${IMAGE_REPO}:${IMAGE_TAG} (build arguments redacted)" echo "Building ${IMAGE_REPO}:${IMAGE_TAG} (build arguments redacted)"