Skip to content

Docker: sed placeholder substitution breaks API base URL validation with non-default BACKEND_PORT #502

Description

@azhongzz

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:

  1. Runtime value location -- resolveBase() uses the inlined string as the API URL
  2. 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

  1. Build Docker image (default BACKEND_PORT=8001)
  2. Run container with e.g. BACKEND_PORT=3781 (or any port != 8001)
  3. Open frontend in browser -> console error: placeholder was not substituted
  4. Settings page shows: NEXT_PUBLIC_API_BASE is not configured
  5. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions