morphit/ops/systemd/morphit-indexer.service
Morphit Team 9cb6cfb77b
Some checks failed
morphit-release / Build + publish release tarball (push) Has been cancelled
Morphit v1.10.8
2026-08-09 00:44:46 -07:00

77 lines
3.3 KiB
Desktop File

[Unit]
# Morphit indexer — Blurt-chain -> Postgres materialisation + read-only REST
# API (/v1/*). Matches the morphit-ops deployment: monorepo at /opt/morphit,
# root-owned config, Postgres in a Docker container.
#
# Install (one-time, as root):
# cp ops/systemd/morphit-indexer.service /etc/systemd/system/
# systemctl daemon-reload
# systemctl enable --now morphit-indexer
# journalctl -u morphit-indexer -f # watch it boot + sync
#
# After this, `morphit-ops upgrade` restarts it automatically and it survives
# reboots — no screen sessions.
Description=Morphit indexer (Blurt-chain indexer and REST API)
Documentation=https://git.agorise.net/agorise/morphit
After=network-online.target docker.service
Wants=network-online.target
# Reboot recovery (cp597): disable the start-rate-limit so a power-cut restart
# never latches this unit into a permanent 'failed' state while Postgres (a
# Docker container here) is still coming up — it keeps retrying every RestartSec
# until the DB is reachable, with no ceiling on attempts.
StartLimitIntervalSec=0
[Service]
Type=simple
# Matches the current deployment (install tree + operator config are
# root-owned). To de-privilege, chown /opt/morphit's config + data to a
# dedicated service user and set User=/Group= here instead.
User=root
WorkingDirectory=/opt/morphit/apps/indexer
# Source the operator env + config exactly like a manual start, then exec the
# indexer. A shell wrapper (rather than EnvironmentFile=) is deliberate so env
# files written in either `KEY=value` or `export KEY=value` form both load.
# We source BOTH layouts, each guarded with `[ -f ]`: the ops-cli layout
# (/opt/morphit/morphit.env + morphit.config.env, written by `morphit-ops
# init`) AND the Ansible layout (/etc/morphit/indexer.env, written by the
# playbook). Whichever a given install produced is sourced; the other is
# simply absent. loadConfig() reads process.env, so either path works.
ExecStart=/usr/bin/env bash -c 'set -a; for f in /opt/morphit/morphit.env /opt/morphit/morphit.config.env /etc/morphit/indexer.env; do [ -f "$f" ] && . "$f"; done; set +a; exec /opt/morphit/node_modules/.bin/tsx src/main.ts'
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=morphit-indexer
# --- Hardening (safe with User=root) -------------------------------
NoNewPrivileges=yes
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectProc=invisible
# cp683 — ProcSubset MUST be 'all', not 'pid'. The indexer computes the
# /v1/health system block (cpu_pct, mem) from /proc/stat + /proc/meminfo; with
# ProcSubset=pid the kernel hides those non-process files, so os.cpus() can't
# read CPU counters (cpu_pct stays permanently null) and /proc/meminfo reads
# fail. These are non-sensitive system-wide stats; ProtectProc=invisible above
# still hides other processes' details, which is the meaningful protection.
ProcSubset=all
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictNamespaces=yes
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# A core dump could contain secrets read from env at boot.
LimitCORE=0
# Node's V8 JIT writes+executes in memory, so W^X must stay off.
MemoryDenyWriteExecute=no
[Install]
WantedBy=multi-user.target