# /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