Bug Description
When running the Docker image with a non-default BACKEND_PORT (e.g., 3781 instead of 8001), the frontend always throws NEXT_PUBLIC_API_BASE placeholder was not substituted in the browser console. The settings page shows NEXT_PUBLIC_API_BASE is not configured, and sessions are not displayed.
Root Cause
The Dockerfile builds the Next.js frontend with a placeholder value for NEXT_PUBLIC_API_BASE:
# Dockerfile, line 47 (frontend-builder stage)
RUN echo "NEXT_PUBLIC_API_BASE=__NEXT_PUBLIC_API_BASE_PLACEHOLDER__" > .env.local
At build time, Turbopack inlines process.env.NEXT_PUBLIC_API_BASE into the JS bundle. The placeholder appears in two contexts:
- Runtime value location --
resolveBase() uses the inlined string as the API URL
- Validation guard location -- Turbopack injects a check:
if(!e || e === "__NEXT_PUBLIC_API_BASE_PLACEHOLDER__") throw ...
At container startup, start-frontend.sh (line 266-267) runs:
find /app/web/.next -type f \( -name "*.js" -o -name "*.json" \) -exec \
sed -i "s|__NEXT_PUBLIC_API_BASE_PLACEHOLDER__|${API_BASE}|g" {} \;
This replaces all occurrences of the placeholder -- including the comparison constant inside the validation guard. After sed:
resolveBase() correctly returns http://localhost:${BACKEND_PORT} (correct)
- The validation guard becomes
if(!e || e === "http://localhost:3781") -- always true -> always throws (broken)
Additional Issue: Hardcoded http://localhost:8001
Some built modules reference http://localhost:8001 directly (the build-time default port from BACKEND_PORT=8001) instead of using the placeholder, so the sed above does not reach them. These modules keep pointing to the wrong port.
Reproduction Steps
- Build Docker image (default
BACKEND_PORT=8001)
- Run container with e.g.
BACKEND_PORT=3781 (or any port != 8001)
- Open frontend in browser -> console error:
placeholder was not substituted
- Settings page shows:
NEXT_PUBLIC_API_BASE is not configured
- Sessions list is empty
Impact
Any user running on a non-default port is affected. This is particularly common on Windows where ports 7930-8129 are often reserved by the system (netsh excluded port range), making the default 8001/8002 ports unusable without manual reconfiguration.
Proposed Fix
Add two additional sed commands after the existing placeholder replacement in start-frontend.sh (after line 267):
# 1. Fix hardcoded default port 8001 in modules that don't use the placeholder
find /app/web/.next -type f -name "*.js" -exec \
sed -i "s|http://localhost:8001|${API_BASE}|g" {} \;
# 2. Fix validation guard by breaking the identity comparison (client chunks only)
# The validation check uses `let t="<API_BASE>"` and compares with `e === t`.
# After sed replaces the placeholder, both sides equal the actual URL, so
# the check always fires. Breaking the comparison variable prevents this.
find /app/web/.next/static/chunks -type f -name "*.js" -exec \
sed -i "s|let t=\"${API_BASE}\"|let t=\"__VALIDATED__\"|g" {} \; 2>/dev/null || true
Note: The second fix targets only static/chunks/ (client-side bundles). The validation guard does not exist in server-side (SSR) chunks.
Alternative Long-term Fix
Use Turbopack's define configuration to replace process.env.NEXT_PUBLIC_API_BASE at build time with a runtime expression, eliminating the need for post-build sed substitution entirely.
Workaround (manual, inside running container)
find /app/web/.next -name '*.js' -exec sed -i 's|http://localhost:8001|http://localhost:3781|g' {} +
find /app/web/.next/static/chunks -name '*.js' -exec sed -i 's|let t="http://localhost:3781"|let t="__VALIDATED__"|g' {} +
Environment
- Host OS: Windows (Docker Desktop)
- Container: DeepTutor Docker image v1.3.6 (ghcr.io/hkuds/deeptutor:latest)
- Dockerfile: multi-stage (Node 22-slim frontend builder + Python 3.11-slim runtime)
Bug Description
When running the Docker image with a non-default
BACKEND_PORT(e.g.,3781instead of8001), the frontend always throwsNEXT_PUBLIC_API_BASE placeholder was not substitutedin the browser console. The settings page showsNEXT_PUBLIC_API_BASE is not configured, and sessions are not displayed.Root Cause
The Dockerfile builds the Next.js frontend with a placeholder value for
NEXT_PUBLIC_API_BASE:At build time, Turbopack inlines
process.env.NEXT_PUBLIC_API_BASEinto the JS bundle. The placeholder appears in two contexts:resolveBase()uses the inlined string as the API URLif(!e || e === "__NEXT_PUBLIC_API_BASE_PLACEHOLDER__") throw ...At container startup,
start-frontend.sh(line 266-267) runs:This replaces all occurrences of the placeholder -- including the comparison constant inside the validation guard. After sed:
resolveBase()correctly returnshttp://localhost:${BACKEND_PORT}(correct)if(!e || e === "http://localhost:3781")-- always true -> always throws (broken)Additional Issue: Hardcoded
http://localhost:8001Some built modules reference
http://localhost:8001directly (the build-time default port fromBACKEND_PORT=8001) instead of using the placeholder, so the sed above does not reach them. These modules keep pointing to the wrong port.Reproduction Steps
BACKEND_PORT=8001)BACKEND_PORT=3781(or any port != 8001)placeholder was not substitutedNEXT_PUBLIC_API_BASE is not configuredImpact
Any user running on a non-default port is affected. This is particularly common on Windows where ports 7930-8129 are often reserved by the system (netsh excluded port range), making the default 8001/8002 ports unusable without manual reconfiguration.
Proposed Fix
Add two additional sed commands after the existing placeholder replacement in
start-frontend.sh(after line 267):Note: The second fix targets only
static/chunks/(client-side bundles). The validation guard does not exist in server-side (SSR) chunks.Alternative Long-term Fix
Use Turbopack's
defineconfiguration to replaceprocess.env.NEXT_PUBLIC_API_BASEat build time with a runtime expression, eliminating the need for post-build sed substitution entirely.Workaround (manual, inside running container)
Environment