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
@@ -0,0 +1,90 @@
|
||||
# /etc/systemd/system/cashumints-web.service
|
||||
#
|
||||
# The frontend is static: `output: 'static'` in astro.config.mjs, and the whole site is
|
||||
# produced ahead of time. There is no frontend build to keep alive, so this unit is a
|
||||
# build rather than a daemon — one shot of `pnpm build`, which compiles shared/, renders
|
||||
# a social card per mint and prerenders every page from the live API. The daemon that
|
||||
# hands the result out is cashumints-site.service.
|
||||
#
|
||||
# Run it after a deploy:
|
||||
# sudo systemctl start cashumints-web
|
||||
#
|
||||
# Mint pages are prerendered, so new mints and new review counts only appear at the
|
||||
# next build; pair this with a .timer for the nightly rebuild. There is deliberately no
|
||||
# [Install] section — a rebuild should be scheduled, not fired on every boot.
|
||||
|
||||
[Unit]
|
||||
Description=Rebuild the cashumints.space static site
|
||||
# Every page's data comes from the API over loopback, so the API has to be up.
|
||||
# Requires= rather than Wants=: a dead API should abort the build, not replace a good
|
||||
# site with an empty one.
|
||||
Requires=cashumints.service
|
||||
After=cashumints.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=cashumints
|
||||
Group=cashumints
|
||||
WorkingDirectory=/home/cashumints/CashuMints.space
|
||||
|
||||
# Where the published copy lands. Shared with the API and the site server, and created
|
||||
# by systemd with this unit's ownership if it is not there yet.
|
||||
StateDirectory=cashumints
|
||||
|
||||
Environment=NODE_ENV=production
|
||||
# Where the build reaches the API. Must match PORT= in cashumints.service.
|
||||
Environment=API_URL=http://127.0.0.1:8788
|
||||
Environment=SITE_URL=https://cashumints.space
|
||||
# Browser-facing origin. Empty means same origin: islands fetch /api/... and nginx
|
||||
# forwards it. Set this only if the API ever moves to its own hostname. Declared here
|
||||
# even though it is empty, because systemd's environment wins over .env — so what a
|
||||
# production build emits cannot drift with an edit to that file.
|
||||
Environment=PUBLIC_API_URL=
|
||||
|
||||
# After= orders the start; it does not wait for the port to accept connections. At boot
|
||||
# the API is still opening its database and probing, so block until it reports healthy
|
||||
# rather than letting the first fetch die on ECONNREFUSED. /api/health answers 503 until
|
||||
# it is genuinely ready, and curl -f treats that as a failure, so the loop keeps waiting.
|
||||
ExecStartPre=/usr/bin/timeout 90 /bin/sh -c 'until curl -sf -o /dev/null http://127.0.0.1:8788/api/health; do sleep 1; done'
|
||||
# Check `which pnpm` on the host: a corepack or pnpm-home install sits outside /usr/bin,
|
||||
# and systemd's PATH does not include it.
|
||||
ExecStart=/usr/bin/pnpm build
|
||||
|
||||
# Publish, as a separate step from building.
|
||||
#
|
||||
# `astro build` empties dist before it writes, so the site server cannot read dist
|
||||
# directly — a nightly rebuild would be a nightly minute of 404s. It serves this copy
|
||||
# instead, and the copy is only touched once a build has succeeded: a failed build
|
||||
# leaves the previous site up rather than replacing it with a half-written one, which is
|
||||
# the same reason Requires=cashumints.service is above.
|
||||
#
|
||||
# --delay-updates stages the changed files and renames them in at the end, so the window
|
||||
# where the tree is a mix of two builds is a rename rather than a whole transfer, and
|
||||
# --delete-after keeps removals from landing before their replacements. Unchanged files
|
||||
# — every hashed asset and card, which is nearly all of it — are not touched at all.
|
||||
ExecStartPost=/usr/bin/rsync -a --delete-after --delay-updates web/dist/ /var/lib/cashumints/web/
|
||||
|
||||
# ~200 prerendered pages plus a card per mint. Minutes, not seconds, on a small VPS, and
|
||||
# TimeoutStartSec is what bounds a Type=oneshot.
|
||||
TimeoutStartSec=1800
|
||||
# A nightly rebuild should not starve the API it is reading from.
|
||||
Nice=10
|
||||
|
||||
# The site server runs as cashumints and reads its own files, so this no longer has to
|
||||
# be world-readable — it was 0022 for nginx, back when nginx opened the files as
|
||||
# www-data. Kept at 0022 anyway: rsync preserves these modes into the published copy,
|
||||
# and a readable static site is easier to inspect than one that needs sudo.
|
||||
UMask=0022
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
# ProtectHome is deliberately absent, unlike in cashumints.service: this unit writes
|
||||
# inside /home/cashumints — web/dist, web/public/og, web/src/generated and the pnpm
|
||||
# store are all under it.
|
||||
ProtectSystem=full
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
Reference in New Issue
Block a user