From e27e31bfa557e591b25101a9c2b6832cfa21f92a Mon Sep 17 00:00:00 2001 From: Hisham Aburaqibah Date: Sun, 20 Sep 2026 11:33:02 +0200 Subject: [PATCH] Bind the production web server to a chosen port react-router-serve only honours a port it is given: with PORT unset it calls getAvailablePort(3000) and takes the first free port from 3000 upwards (@react-router/serve cli.js). Nothing in the manual path set PORT, so a busy 3000 silently moved the site to 3001, 3002 and so on, and a reverse proxy configured against one upstream stopped matching. Docker never showed this because apps/web/Dockerfile and docker-compose.yml both set PORT=3000. The web start script now always passes a port - PORT if exported, else WEB_PORT from .env, else 3000 - so the manual path is as deterministic as the containerised one and .env's WEB_PORT finally reaches the process that binds. HOST is deliberately left undefaulted: react-router-serve reads it directly, unset means every interface (what the container needs) and 127.0.0.1 restricts the socket to a proxy on the same host. Verified against the existing build: PORT=8123 listens on *:8123, and WEB_PORT=8124 HOST=127.0.0.1 listens on 127.0.0.1:8124 and serves /en with a 200 while the CMS is down. .env.example documents both variables, and the deployment section of README.md gains a systemd unit, since nothing sources .env there, and a warning that WEB_PORT is the published host port under Docker but the listen port in manual mode. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 11 +++++++++++ README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- apps/web/package.json | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 63e5537..2841ca1 100644 --- a/.env.example +++ b/.env.example @@ -68,7 +68,18 @@ ENCRYPTION_KEY= # --------------------------------------------------------------------------- # React Router server (production build / Docker). `pnpm dev:web` uses 5173. +# Manual mode: `pnpm --filter web start` binds this port exactly - export it +# first (set -a; . ./.env; set +a) or pass it inline (PORT=8080 pnpm ...). +# Without it the server would pick the first free port from 3000 upwards, +# which a reverse proxy cannot be pointed at. WEB_PORT=3000 + +# Manual mode only - interface the React Router server binds to. Unset means +# every interface. Set it to 127.0.0.1 when a reverse proxy on the same host +# is the only thing that should reach it. Leave it unset under Docker, where +# the container's own network namespace already isolates it. +# HOST=127.0.0.1 + # Strapi API + admin panel. CMS_PORT=1337 diff --git a/README.md b/README.md index c8a45c2..c9b9365 100644 --- a/README.md +++ b/README.md @@ -469,7 +469,7 @@ docker run --rm -v sharper_cms-uploads:/data -v "$PWD":/backup alpine \ | --- | --- | | `dev` | `react-router dev` — SSR dev server with HMR | | `build` | `react-router build` → `build/client` (static assets) + `build/server` (SSR bundle) | -| `start` | `react-router-serve ./build/server/index.js` — the production server, port 3000 | +| `start` | the production server — `react-router-serve ./build/server/index.js` on `PORT`, falling back to `WEB_PORT`, then 3000 | | `typecheck` | regenerates `+types/*` route types, then `tsc` | **`apps/cms`** — run with `pnpm --filter cms