Give the web server its own .env, where PORT means its port

The previous commit made `pnpm --filter web start` load the repository-root
.env, which was the wrong file: Strapi reads a bare PORT as well
(apps/cms/config/server.ts uses env.int('PORT', 1337)), so one value there
would have moved the CMS onto the website's port the moment anyone exported
it - and the root .env is docker compose's file, whose WEB_PORT is a
published host port, not a listen port.

The web app now loads apps/web/.env, the way Strapi already loads
apps/cms/.env. PORT and HOST are per-application because they mean something
different to each server. apps/web/.env.example documents them together with
SITE_URL, STRAPI_URL and STRAPI_TOKEN, and warns against the VITE_ prefix,
which would inline a value into the client bundle - STRAPI_TOKEN lives in
that file.

Still no default port: with PORT unset, react-router-serve keeps its own
behaviour of taking the first free port from 3000 upwards. A real
environment variable wins over the file, so a systemd unit or a container
that injects PORT is unaffected.

.dockerignore now excludes .env at every depth, not only at the root.
`COPY apps/web ./apps/web` would otherwise have baked a developer's
apps/web/.env - STRAPI_TOKEN included - into the image.

Verified against the existing build: PORT=8140 HOST=127.0.0.1 in
apps/web/.env listens on 127.0.0.1:8140, and PORT=8141 in the environment
overrides the file while HOST still comes from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-20 11:45:29 +02:00
co-authored by Claude Opus 5
parent e27e31bfa5
commit 2ed3a17914
5 changed files with 110 additions and 70 deletions
+16 -23
View File
@@ -7,12 +7,13 @@
# WHO READS THIS FILE
# * docker compose - every ${VARIABLE} in docker-compose.yml is read from
# the .env file sitting next to it (this one).
# * apps/web - the React Router server reads STRAPI_URL / STRAPI_TOKEN
# / SITE_URL from its process environment. In manual mode
# nothing loads this file for you, so export it first:
# set -a; . ./.env; set +a
# The defaults below are also the code defaults, so a
# plain `pnpm dev` works without exporting anything.
# * apps/web - NOT this file. In manual mode the web app reads
# apps/web/.env (cp apps/web/.env.example apps/web/.env),
# which is where PORT, HOST, SITE_URL, STRAPI_URL and
# STRAPI_TOKEN belong. Under Docker the values below are
# injected into the container instead. The defaults are
# also the code defaults, so a plain `pnpm dev` works
# with no .env at all.
# * apps/cms - Strapi does NOT read this file. In manual mode it reads
# apps/cms/.env (cp apps/cms/.env.example apps/cms/.env).
# Under Docker the values below are injected into the
@@ -63,23 +64,18 @@ ENCRYPTION_KEY=
# ---------------------------------------------------------------------------
# PORTS
# Host ports the two applications are published on in Docker mode, and the
# ports they listen on in manual mode.
# Host ports the two applications are published on in Docker mode. The ports
# they LISTEN on in manual mode live in each application's own .env
# (apps/web/.env, apps/cms/.env), because both servers read a bare PORT.
# ---------------------------------------------------------------------------
# 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.
# Docker mode - host port the web container is published on; inside the
# container it always listens on 3000, so WEB_PORT=8080 means 8080:3000.
# Manual mode has its own PORT in apps/web/.env - never put a bare PORT in
# THIS file: both servers would read it and Strapi would land on the website's
# port. `pnpm dev:web` uses 5173 either way.
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
@@ -90,7 +86,7 @@ CMS_PUBLIC_URL=http://localhost:1337
# ---------------------------------------------------------------------------
# WEB APP
# WEB APP - Docker mode. In manual mode these live in apps/web/.env.
# ---------------------------------------------------------------------------
# Public origin of the site. Used for canonical URLs, hreflang alternates and
@@ -98,9 +94,6 @@ CMS_PUBLIC_URL=http://localhost:1337
# Leave it unset in local development and the request's own host is used.
SITE_URL=http://localhost:3000
# Where the server-side loaders fetch content from, MANUAL MODE.
STRAPI_URL=http://localhost:1337
# Where they fetch from inside Docker, where "cms" is the compose service name.
# NOTE: this origin is also prefixed onto uploaded media URLs, which the
# browser has to be able to resolve too - see "Images from the CMS" in the