catcrafts.net/deploy/catcrafts-server.service
Jorijn van der Graaf 2fa6e70af1
All checks were successful
Deploy / build-deploy (push) Successful in 6m14s
order confirmation
2026-08-09 00:14:09 +02:00

94 lines
4.1 KiB
Desktop File

# catcrafts-server — the server-rendering backend.
#
# Install to /etc/systemd/system/catcrafts-server.service, then:
# systemctl daemon-reload && systemctl enable --now catcrafts-server
#
# Layout this expects on the host:
# /srv/catcrafts.net/ the wasm bundle + static assets (Caddy's root,
# and the rsync --delete target from CI)
# /srv/catcrafts-app/ the server binary and content/, deployed by CI
# catcrafts-server
# content/{projects,posts}.json
# /var/lib/catcrafts/ runtime state — the SQLite database and keys
# once the shop exists. NEVER in the webroot:
# that directory is both publicly served and
# wiped by `rsync --delete` on every deploy.
[Unit]
Description=catcrafts.net server-rendering backend
Documentation=https://forgejo.catcrafts.net/Catcrafts/catcrafts.net
After=network-online.target
Wants=network-online.target
# Caddy proxies to this; if it is down Caddy falls back to the static shell, so
# there is no hard ordering requirement between them.
[Service]
Type=simple
User=catcrafts
Group=catcrafts
WorkingDirectory=/srv/catcrafts-app
# --webroot points at Caddy's root so the boot <script> tags (with their
# per-build ?v= cache buster) are read from the deployed index.html rather than
# hardcoded. Bind to loopback only: Caddy terminates TLS and this speaks
# plaintext HTTP/1.1.
ExecStart=/srv/catcrafts-app/catcrafts-server --serve 8081 \
--content=/srv/catcrafts-app/content \
--webroot=/srv/catcrafts.net \
--orders=/var/lib/catcrafts/orders.jsonl \
--bunq-state=/var/lib/catcrafts/bunq-state.json
Restart=always
RestartSec=2s
# ── hardening ────────────────────────────────────────────────────────────
# This process will later hold bank and payment credentials, so it gets locked
# down now rather than after there is something worth stealing.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
# Only IP sockets — no unix, no netlink, no packet sockets.
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# ProtectSystem=strict makes everything read-only; grant just the state
# directory. StateDirectory creates /var/lib/catcrafts with the right owner.
StateDirectory=catcrafts
StateDirectoryMode=0700
# The content and webroot are read-only to this process by design: content is
# generated at build time and the webroot belongs to the deploy step.
ReadOnlyPaths=/srv/catcrafts-app /srv/catcrafts.net
# Secrets arrive from OUTSIDE the deployed tree — the web root is public and
# rsync-wiped, and /srv/catcrafts-app is CI-writable; neither may ever hold a
# credential. /etc/catcrafts/payments.env (root:root 0600) carries:
# MOLLIE_API_KEY=live_... (or test_... while verifying) — the rail
# SENDCLOUD_PUBLIC_KEY / SENDCLOUD_SECRET_KEY / SENDCLOUD_METHOD — optional,
# live shipping rates; zone table without them
# INVOICE_GPG_KEY=... invoice signing (see deploy/README.md)
# MAIL_COMMAND=msmtp -t order confirmation email (see deploy/README.md,
# MAIL_FROM=... "Order email"); unset = no email is sent
# BUNQ_API_KEY=... legacy: only used when no Mollie key is set
# The '-' prefix makes the file optional: without it the server starts with
# payments off and the shop renders but refuses checkout — degraded, not down.
EnvironmentFile=-/etc/catcrafts/payments.env
EnvironmentFile=-/etc/catcrafts/bunq.env
# Invoice signing keyring (see deploy/README.md, "Invoice signing").
Environment=GNUPGHOME=/var/lib/catcrafts/gnupg
[Install]
WantedBy=multi-user.target