# /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: # - WatchdogSec: systemd kills and restarts a hung Gunicorn within 30 s # - 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 # Watchdog: systemd sends SIGKILL if Gunicorn doesn't send keepalives within 30 s. # Requires gunicorn to be started with --preload OR the watchdog plugin; here we # rely on the worker timeout (25 s) to recycle hung workers before the 30 s # watchdog fires, which restarts the entire service. 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