CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
83 lines
3.1 KiB
Desktop File
83 lines
3.1 KiB
Desktop File
# /etc/systemd/system/passkeeper.service
|
|
#
|
|
# Install / update:
|
|
# sudo cp scripts/passkeeper.service /etc/systemd/system/passkeeper.service
|
|
# sudo systemctl daemon-reload
|
|
# sudo systemctl enable passkeeper
|
|
# sudo systemctl restart passkeeper
|
|
# journalctl -xeu passkeeper.service
|
|
#
|
|
# Phase 5 additions vs original:
|
|
# - Restart=on-failure: systemd restarts Gunicorn if the master exits non-zero
|
|
# - Gunicorn settings live in gunicorn.conf.py (gthread workers, timeout
|
|
# ordered above nginx proxy_read_timeout, /dev/shm heartbeat dir)
|
|
# - PrivateTmp, NoNewPrivileges, ProtectSystem: basic systemd sandboxing
|
|
# - StartLimitIntervalSec / StartLimitBurst: caps restart storm
|
|
|
|
[Unit]
|
|
Description=PassKeeper Gunicorn daemon
|
|
After=network.target mysql.service
|
|
Wants=mysql.service
|
|
|
|
# Restart policy: cap to 5 restarts in 60 s to prevent restart storms
|
|
StartLimitIntervalSec=60
|
|
StartLimitBurst=5
|
|
|
|
[Service]
|
|
User=www-data
|
|
Group=www-data
|
|
WorkingDirectory=/home/spuser/PassKeeper
|
|
EnvironmentFile=/home/spuser/PassKeeper/.env
|
|
|
|
# All tuning lives in gunicorn.conf.py (worker class, counts, timeouts,
|
|
# logging) so it is versioned with the code and documented in one place.
|
|
# Override any of it with GUNICORN_* variables in the EnvironmentFile above
|
|
# rather than editing this line.
|
|
# Absolute path — do not rely on WorkingDirectory for config lookup.
|
|
ExecStart=/home/spuser/.venv/bin/gunicorn \
|
|
-c /home/spuser/PassKeeper/gunicorn.conf.py \
|
|
wsgi:app
|
|
|
|
# Reload: HUP re-reads config and restarts workers in place under the
|
|
# existing master.
|
|
#
|
|
# NOT USR2. USR2 forks a *second* master that inherits the listening socket,
|
|
# and retiring the old one needs a follow-up WINCH + QUIT that this unit never
|
|
# sent. The result was two masters competing for the port with systemd $MAINPID
|
|
# tracking the stale one — a later stop/restart then signalled the wrong
|
|
# process and the socket vanished, which Nginx reports as 502.
|
|
#
|
|
# HUP does not pick up changed Python source: use `systemctl restart` for code
|
|
# deploys, reload only for config-only changes.
|
|
ExecReload=/bin/kill -s HUP $MAINPID
|
|
|
|
# NO WatchdogSec here — deliberately.
|
|
#
|
|
# WatchdogSec requires the service to send WATCHDOG=1 keepalives over the sd_notify
|
|
# socket. Gunicorn only does that when systemd exports NOTIFY_SOCKET, which happens
|
|
# only under Type=notify (+ NotifyAccess=main). This unit is Type=simple (the
|
|
# default), so no keepalive was ever sent, systemd treated the service as hung, and
|
|
# SIGKILLed it every ~30 s. Restart=on-failure then brought it back after RestartSec,
|
|
# producing a repeating window of 502s from Nginx.
|
|
#
|
|
# Hung *workers* are already handled by Gunicorn's own `timeout` in gunicorn.conf.py; a crashed
|
|
# *master* is already handled by Restart=on-failure below. The watchdog added no
|
|
# coverage, only outages.
|
|
#
|
|
# To re-enable it properly (optional), all three lines are required:
|
|
# Type=notify
|
|
# NotifyAccess=main
|
|
# WatchdogSec=30s
|
|
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
|
|
# Systemd sandboxing
|
|
PrivateTmp=true
|
|
NoNewPrivileges=true
|
|
ProtectSystem=strict
|
|
ReadWritePaths=/home/spuser/PassKeeper /home/spuser/logs /home/spuser/backups
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|