# Installed to ~/.config/systemd/user/ by scripts/install_service.sh, which # substitutes @REPO_ROOT@. Edit this template, never the installed copy. [Unit] Description=Signal group bot Documentation=file://@REPO_ROOT@/README.md # The bot's only outbound dependency is signal-cli-rest-api in Docker. This # ordering is worth having and is NOT a fix: dockerd being up says nothing # about whether the container inside it has finished starting, and signal-cli # then spends further minutes loading the account. systemd cannot express # "after that container is ready", so the bot retries the connection itself # (signal_bot/startup.py) and serves an unhealthy /healthz meanwhile. After=network-online.target docker.service Wants=network-online.target # The start limit now only has to catch answers, not outages. A config error # -- a missing PHONE_NUMBER, a taken port, a container in the wrong mode -- # exits almost instantly and would otherwise spin forever, so five of those # stop the unit in `failed`, which shows up in `systemctl --user --failed`. # # The arithmetic: 5 starts x RestartSec=30 is 120s, comfortably inside the # 300s window, so a genuine config error still trips the limit. What used to # trip it was 5 x 5s = 25s of a container being slow to boot; on 2026-08-30 # that left the group with no bot for eight hours. StartLimitIntervalSec=300 StartLimitBurst=5 [Service] Type=exec WorkingDirectory=@REPO_ROOT@ # run.sh execs the interpreter, so this PID is the bot itself. ExecStart=@REPO_ROOT@/run.sh # `always`, not `on-failure`: the bot never exits on its own except when # startup hits something no retry can fix, and that path asks uvicorn to # shut down cleanly -- so exit 0 must still count as a restart. Restart=always RestartSec=30 # The bot writes its SQLite tally database inside the repo, and papal # portraits into the cache directory. CacheDirectory= is load-bearing: # ProtectHome=read-only makes the rest of $HOME immutable, and systemd # exempts only the directory it created, exporting it as $CACHE_DIRECTORY. CacheDirectory=signal-bot NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=@REPO_ROOT@ # The photo album. ProtectHome=read-only covers it too, and !add_photo # writes there -- media, tags.json, custom.json and adds.json. The pack # does not move; this is the one path that has to become writable. ReadWritePaths=%h/.local/share/signal-bot ProtectKernelTunables=true ProtectControlGroups=true RestrictSUIDSGID=true [Install] WantedBy=default.target