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