Compile the API instead of running its TypeScript in production.
The unit's ExecStart named src/index.ts, so every start depended on the host having Node 22.18 or newer for native type stripping. A deploy onto a host with Node 20 met ERR_UNKNOWN_FILE_EXTENSION, exited in under a second, and was restarted 464 times over fifteen hours with nothing anywhere going red. api/tsconfig.json now emits to api/dist. The source keeps its explicit .ts import specifiers, which is what makes `node --watch src/index.ts` work in development; rewriteRelativeImportExtensions turns them into .js on the way out, so what runs in production is ordinary ESM that any Node from 20.18 up will start. `pnpm build` builds shared, then api, then web. `pnpm dev` is unchanged. deploy/ is tracked rather than ignored: the unit files are the thing an operator copies to /etc/systemd/system, and the alert unit added next has to live somewhere a deploy can find it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
06ba3d35e7
commit
65307ba278
@@ -52,9 +52,15 @@ rating encoding, and the bugs this rebuild fixes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node 22.18 or newer (native TypeScript type stripping, so no build step for the API)
|
||||
- Node 20.18 or newer to build and to run what a build produces
|
||||
- Node 22.18 or newer to *develop*: `pnpm dev`, `pnpm seed` and the `api` test scripts
|
||||
run `src/*.ts` through node directly, which needs native type stripping
|
||||
- pnpm 9 or newer
|
||||
|
||||
`engines.node` is the first of those, not the second, on purpose: it is the floor a
|
||||
deployment has to clear, and a production host should never be told it needs a newer
|
||||
Node than the compiled service actually runs on.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
@@ -286,7 +292,11 @@ pnpm typecheck
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Output lands in `web/dist/`. Every page is prerendered once per language, so ~55 mints
|
||||
Three packages in order, and the order is a dependency chain rather than a habit:
|
||||
`shared` emits the types and the warning copy both other packages import, `api` compiles
|
||||
`api/src` to `api/dist`, and `web` prerenders against a running API.
|
||||
|
||||
Output lands in `api/dist/` and `web/dist/`. Every page is prerendered once per language, so ~55 mints
|
||||
and 9 static routes come out as ~200 pages, each with real titles, meta descriptions,
|
||||
OpenGraph and Twitter tags, a social card, a self-referencing canonical, a full hreflang
|
||||
set and a JSON-LD graph. `sitemap.xml` lists every indexable one with its `xhtml:link`
|
||||
@@ -782,6 +792,9 @@ literal specified behaviour. `shared/src/score.ts` carries the arithmetic, and
|
||||
|
||||
## Deployment
|
||||
|
||||
The unit files and the nginx block quoted below are checked in under `deploy/`. Those
|
||||
are the copies to edit; what is quoted here is the same text, for reading in context.
|
||||
|
||||
Three units and an nginx block. Everything this project runs listens on loopback and
|
||||
runs as the same unprivileged user; nginx terminates TLS and proxies to it, and opens no
|
||||
file belonging to the project.
|
||||
@@ -806,19 +819,34 @@ is the one that built them.
|
||||
|
||||
### Node
|
||||
|
||||
The API and the site server both run TypeScript and ESM directly, with no build step, so
|
||||
**systemd's node must be 22.18 or newer** — that is the release where native type
|
||||
stripping stopped needing a flag. This is not the same question as `node -v` in your
|
||||
shell: a version manager puts its node on the interactive `PATH` only, while systemd
|
||||
resolves the absolute path in `ExecStart`. Check the one that matters:
|
||||
**Node 20.18 or newer is enough.** Nothing systemd starts reads a `.ts` file: `pnpm
|
||||
build` compiles `api/src` to `api/dist`, the site server is plain `.mjs`, and both units
|
||||
run `/usr/bin/node` against ordinary JavaScript. 20.18 rather than 20.0 only because
|
||||
both `ExecStart` lines pass `--env-file-if-exists`, which landed there.
|
||||
|
||||
```bash
|
||||
/usr/bin/node --version
|
||||
```
|
||||
|
||||
On Node 20 the API exits immediately with `ERR_UNKNOWN_FILE_EXTENSION` for `.ts` and
|
||||
restarts forever. Install Node system-wide rather than pointing `ExecStart` at a version
|
||||
manager's path, which breaks at the next upgrade and is invisible to `ProtectHome`.
|
||||
That is the version that matters, and it is not the same question as `node -v` in your
|
||||
shell: a version manager puts its node on the interactive `PATH` only, while systemd
|
||||
resolves the absolute path in `ExecStart`. Install Node system-wide rather than pointing
|
||||
`ExecStart` at a version manager's path, which breaks at the next upgrade and is
|
||||
invisible to `ProtectHome`.
|
||||
|
||||
**Why this section used to say 22.18.** The API ran `src/index.ts` directly, on native
|
||||
type stripping, so the host's Node version was a runtime dependency of the service. A
|
||||
deploy onto a host with Node 20 met `ERR_UNKNOWN_FILE_EXTENSION`, exited in under a
|
||||
second, and was restarted by systemd 464 times over fifteen hours. Every dashboard was
|
||||
green throughout, because there was no dashboard: `Restart=on-failure` with no start
|
||||
limit is an infinite loop that never reports a failure. Two things changed. The service
|
||||
is compiled, so the host's Node version cannot break it in that way again; and the units
|
||||
now stop after five failures in two minutes and run an `OnFailure=` alert, so if
|
||||
something else breaks it in some other way, the machine says so. See "Failing loudly".
|
||||
|
||||
The version floor that is still 22.18 is the *development* one — `pnpm dev`, `pnpm seed`,
|
||||
`pnpm migrate` and the `api` `test:*` scripts all hand `src/*.ts` to node. That is a
|
||||
laptop requirement, not a server one.
|
||||
|
||||
### The API
|
||||
|
||||
@@ -849,7 +877,10 @@ Environment=DB_PATH=/var/lib/cashumints/cashumints.db
|
||||
Environment=ICON_DIR=/var/lib/cashumints/icons
|
||||
# For Postgres, replace DB_PATH with DATABASE_URL and add After=postgresql.service.
|
||||
# Keep ICON_DIR either way: cached icons are files, not rows.
|
||||
ExecStart=/usr/bin/node --env-file-if-exists=../.env src/index.ts
|
||||
|
||||
# Compiled JavaScript, run by the distribution's own node. See "Node" above for why
|
||||
# this is not src/index.ts any more.
|
||||
ExecStart=/usr/bin/node --env-file-if-exists=../.env dist/index.js
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
@@ -1161,7 +1192,7 @@ Order matters once: the site server refuses to start against a root that has no
|
||||
`index.html`, so the build has to publish before it comes up.
|
||||
|
||||
```bash
|
||||
/usr/bin/node --version # 22.18 or newer, or the API will not run
|
||||
/usr/bin/node --version # 20.18 or newer
|
||||
sudo systemctl enable --now cashumints # API first: the build reads from it
|
||||
sudo systemctl start cashumints-web # build, then publish to /var/lib/cashumints/web
|
||||
sudo systemctl enable --now cashumints-site # now it has something to serve
|
||||
|
||||
Reference in New Issue
Block a user