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:
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user