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:
+6
-4
@@ -14,10 +14,12 @@ apps/cms/.strapi
|
||||
apps/cms/.tmp
|
||||
apps/cms/.cache
|
||||
|
||||
# Secrets. The containers receive their environment from docker compose.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
# Secrets. The containers receive their environment from docker compose, so no
|
||||
# .env belongs in an image - including the per-application ones (apps/web/.env,
|
||||
# apps/cms/.env), which COPY apps/web / COPY apps/cms would otherwise pick up.
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
|
||||
# Licensed fonts are mounted or copied in at deploy time, never baked into a
|
||||
# published image. See the FONTS section of README.md.
|
||||
|
||||
+16
-23
@@ -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
|
||||
|
||||
@@ -195,12 +195,17 @@ sudo -u postgres psql -c "CREATE ROLE sharper WITH LOGIN PASSWORD 'a-strong-pass
|
||||
|
||||
### 4. Environment files
|
||||
|
||||
Two files, with different jobs:
|
||||
Three files, with different jobs:
|
||||
|
||||
| File | Read by | Required in manual mode |
|
||||
| --- | --- | --- |
|
||||
| `apps/cms/.env` | Strapi (it loads its own `.env`) | **yes** |
|
||||
| `.env` (root) | `docker compose`, and optionally the web server | no — the code defaults already point at `localhost` |
|
||||
| `apps/web/.env` | `pnpm --filter web start` (it loads its own `.env`) | no — but it is where `PORT` goes |
|
||||
| `.env` (root) | `docker compose` only | no |
|
||||
|
||||
Each application reads its **own** `.env`, and the root one belongs to `docker compose`.
|
||||
That split is not cosmetic: `PORT` and `HOST` mean something different to each server, and a
|
||||
single shared value would put Strapi on the website's port.
|
||||
|
||||
```bash
|
||||
# CMS: copy the fully documented template, then fill in the six secrets
|
||||
@@ -222,17 +227,19 @@ overwrites a value that is already set unless you pass `--force`, so it is safe
|
||||
Without `--write` it just prints a block you can paste. By hand:
|
||||
`openssl rand -base64 32`, once per key (`APP_KEYS` takes at least two, comma separated).
|
||||
|
||||
The web application reads `STRAPI_URL`, `STRAPI_TOKEN` and `SITE_URL` from its **process
|
||||
environment**. Nothing loads the root `.env` for you in manual mode, and you do not need it:
|
||||
`STRAPI_URL` already defaults to `http://localhost:1337`, and with `SITE_URL` unset the
|
||||
canonical URL is derived from the incoming request. To override them anyway:
|
||||
The web application needs nothing to start: `STRAPI_URL` already defaults to
|
||||
`http://localhost:1337`, and with `SITE_URL` unset the canonical URL is derived from the
|
||||
incoming request. Copy its template when you want to override those, or to pin the
|
||||
production port:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
set -a; . ./.env; set +a # export it into the current shell
|
||||
pnpm dev
|
||||
cp apps/web/.env.example apps/web/.env # PORT, HOST, SITE_URL, STRAPI_URL, STRAPI_TOKEN
|
||||
```
|
||||
|
||||
`pnpm --filter web start` loads that file. `pnpm dev:web` does not — the dev server is Vite
|
||||
on 5173 — so export anything you need for development, or put it in the file for the
|
||||
production server and pass it inline while developing.
|
||||
|
||||
### 5. Run both applications
|
||||
|
||||
```bash
|
||||
@@ -469,7 +476,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` | the production server — `react-router-serve ./build/server/index.js` on `PORT`, falling back to `WEB_PORT`, then 3000 |
|
||||
| `start` | the production server — `react-router-serve ./build/server/index.js`, started with `node --env-file-if-exists=.env` so `apps/web/.env` can set `PORT` |
|
||||
| `typecheck` | regenerates `+types/*` route types, then `tsc` |
|
||||
|
||||
**`apps/cms`** — run with `pnpm --filter cms <script>`:
|
||||
@@ -766,58 +773,49 @@ pnpm install --frozen-lockfile
|
||||
pnpm build # cms (admin panel + dist/) then web (build/client + build/server)
|
||||
|
||||
pnpm --filter cms start # strapi start :1337
|
||||
pnpm --filter web start # react-router-serve :3000
|
||||
pnpm --filter web start # react-router-serve :$PORT (see below)
|
||||
```
|
||||
|
||||
Both are ordinary long-running Node processes. Run them under systemd, PM2 or the
|
||||
containers in `docker-compose.yml`, behind a reverse proxy (nginx, Caddy) that terminates
|
||||
TLS and forwards to 3000 and 1337.
|
||||
|
||||
#### Pinning the web server's port and interface
|
||||
#### Choosing the web server's port
|
||||
|
||||
`react-router-serve` only picks a port itself when `PORT` is unset — and it picks the first
|
||||
free one **from 3000 upwards**, so a busy 3000 silently moves the site to 3001, 3002, … and
|
||||
the proxy's upstream stops matching. The `start` script therefore always passes a port:
|
||||
`PORT`, else `WEB_PORT`, else 3000. Choose one explicitly, any of these ways:
|
||||
`react-router-serve` binds `PORT` when it is set, and otherwise takes the first **free** port
|
||||
from 3000 upwards — so a busy 3000 moves the site to 3001, 3002, … with no error, and the
|
||||
proxy's upstream stops matching. Nothing in the repository defaults it for you.
|
||||
|
||||
`pnpm --filter web start` runs under `node --env-file-if-exists=.env`, so `apps/web/.env` is
|
||||
enough:
|
||||
|
||||
```bash
|
||||
PORT=8080 pnpm --filter web start # one run
|
||||
set -a; . ./.env; set +a # or export WEB_PORT=8080 from .env once
|
||||
pnpm --filter web start
|
||||
echo 'PORT=8080' >> apps/web/.env
|
||||
pnpm --filter web start # :8080
|
||||
```
|
||||
|
||||
`HOST` is read by the server directly and is not defaulted. Unset, it listens on every
|
||||
interface; set it when the reverse proxy on the same host should be the only way in:
|
||||
It has to be that file and not the root `.env`: Strapi reads a bare `PORT` too, so a shared
|
||||
one would move the CMS onto the website's port. `apps/web/.env` is read by the web server
|
||||
alone, exactly as `apps/cms/.env` is read by Strapi alone.
|
||||
|
||||
```bash
|
||||
HOST=127.0.0.1 PORT=8080 pnpm --filter web start
|
||||
```
|
||||
A real environment variable wins over the file, so `PORT=9090 pnpm --filter web start` and a
|
||||
systemd unit's `Environment=PORT=8080` both override it. `HOST` works the same way — unset it
|
||||
listens on every interface, `HOST=127.0.0.1` only accepts the reverse proxy on the same host.
|
||||
|
||||
Under systemd both belong in the unit, where nothing sources `.env` for you:
|
||||
Under Docker none of this applies: `PORT=3000` is set in the image and in
|
||||
`docker-compose.yml`, `WEB_PORT` in the root `.env` is the **host** port the container is
|
||||
published on (`WEB_PORT=8080` gives `8080:3000`), and `.dockerignore` keeps every `.env` out
|
||||
of the build context.
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
Environment=NODE_ENV=production HOST=127.0.0.1 PORT=8080
|
||||
Environment=SITE_URL=https://sharper.ly STRAPI_URL=https://cms.sharper.ly
|
||||
WorkingDirectory=/srv/sharper
|
||||
ExecStart=/usr/bin/pnpm --filter web start
|
||||
```
|
||||
|
||||
Under Docker the container always listens on 3000 (`PORT` is set in `apps/web/Dockerfile`
|
||||
and in `docker-compose.yml`); `WEB_PORT` there is the **host** port it is published on, so
|
||||
`WEB_PORT=8080` gives `8080:3000` and the proxy points at 8080. Leave `HOST` unset in that
|
||||
mode — the container's own network namespace is the isolation.
|
||||
|
||||
Whichever port you pick, `SITE_URL` must still be the public origin visitors use
|
||||
(`https://sharper.ly`), not the internal `http://127.0.0.1:8080` — canonical URLs, `hreflang`
|
||||
and JSON-LD are built from it.
|
||||
Whichever port you pick, `SITE_URL` stays the public origin (`https://sharper.ly`), not
|
||||
`http://127.0.0.1:8080` — canonical URLs, `hreflang` and JSON-LD are built from it.
|
||||
|
||||
### Production environment
|
||||
|
||||
| Variable | Where | Value |
|
||||
| --- | --- | --- |
|
||||
| `NODE_ENV` | both | `production` |
|
||||
| `PORT` | web | the port `react-router-serve` binds, e.g. `8080`. Unset it and the script falls back to `WEB_PORT`, then 3000 |
|
||||
| `PORT` | web | the port `react-router-serve` binds, e.g. `8080`. Unset, it takes the first free port from 3000 upwards |
|
||||
| `HOST` | web | optional; `127.0.0.1` to accept only the local reverse proxy, unset to listen on every interface |
|
||||
| `SITE_URL` | web | `https://sharper.ly` — canonical URLs, `hreflang`, JSON-LD are built from it |
|
||||
| `STRAPI_URL` | web | the CMS origin, e.g. `https://cms.sharper.ly`. It is also prefixed onto media URLs, so it must be resolvable **by the browser**, not only from the server |
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# SHARP-ER web (React Router 8) - environment template
|
||||
#
|
||||
# Copy to .env and fill in real values: cp .env.example .env
|
||||
# Never commit .env.
|
||||
#
|
||||
# `pnpm --filter web start` loads THIS file (apps/web/.env), the same way the
|
||||
# CMS loads apps/cms/.env. It is per-application on purpose: PORT and HOST mean
|
||||
# something different to each server, and a single shared value would move
|
||||
# Strapi onto the website's port. The repository-root .env is docker compose's
|
||||
# file and is not read here.
|
||||
#
|
||||
# A real environment variable always wins over this file, so a systemd unit,
|
||||
# a container or `PORT=9090 pnpm --filter web start` overrides any line below.
|
||||
#
|
||||
# `pnpm dev:web` does NOT read this file - the dev server is Vite on 5173, and
|
||||
# it only exposes variables prefixed VITE_, by inlining them into the CLIENT
|
||||
# bundle. Never use that prefix here: STRAPI_TOKEN below is a server secret.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# --- Server ----------------------------------------------------------------
|
||||
|
||||
# Port react-router-serve binds. Leave it unset and the server takes the first
|
||||
# FREE port from 3000 upwards - 3001, 3002, ... once 3000 is busy - which a
|
||||
# reverse proxy cannot be pointed at. `pnpm dev:web` ignores this and uses 5173.
|
||||
PORT=3000
|
||||
|
||||
# Interface to bind. Unset means every interface. Set it to 127.0.0.1 when a
|
||||
# reverse proxy on the same host should be the only way in.
|
||||
# HOST=127.0.0.1
|
||||
|
||||
# --- Content ---------------------------------------------------------------
|
||||
|
||||
# Public origin of the site. Canonical URLs, hreflang alternates and JSON-LD
|
||||
# are built from it, so it must match how visitors actually reach the site -
|
||||
# https://sharper.ly in production, never the internal http://127.0.0.1:3000.
|
||||
# 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. This origin is also
|
||||
# prefixed onto uploaded media URLs, which the browser has to resolve too -
|
||||
# see "Images from the CMS" in README.md.
|
||||
STRAPI_URL=http://localhost:1337
|
||||
|
||||
# Optional Strapi API token. Only needed to read draft or otherwise protected
|
||||
# content; published content is public. Settings -> API Tokens in the admin.
|
||||
STRAPI_TOKEN=
|
||||
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev --host",
|
||||
"start": "PORT=${PORT:-${WEB_PORT:-3000}} react-router-serve ./build/server/index.js",
|
||||
"start": "node --env-file-if-exists=.env ../../node_modules/.bin/react-router-serve ./build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user