# /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 --timeout: workers that don't respond within 25 s are replaced # - Gunicorn --graceful-timeout: allows in-flight requests to finish on reload # - 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 ExecStart=/home/spuser/.venv/bin/gunicorn \ --workers 4 \ --bind 127.0.0.1:5000 \ --timeout 25 \ --graceful-timeout 20 \ --keep-alive 5 \ --access-logfile /home/spuser/logs/access.log \ --error-logfile /home/spuser/logs/error.log \ --log-level warning \ wsgi:app # Reload (zero-downtime): send USR2 to Gunicorn master ExecReload=/bin/kill -s USR2 $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 above; 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