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>
91 lines
4.4 KiB
Desktop File
91 lines
4.4 KiB
Desktop File
# /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
|