Tools: 5 systemd units for a Python web app, complete and copy-pasteable. No Docker. - Complete Guide

Tools: 5 systemd units for a Python web app, complete and copy-pasteable. No Docker. - Complete Guide

Unit 1 — Flask API server

Unit 2 — Background data collector loop

Unit 3 — Healthcheck monitor with Telegram alerts

Unit 4 — Alerts worker (background dispatch)

Unit 5 — Mission supervision dashboard

How to deploy all 5 in one shot

What you DON'T need

When you DO need more

A note on Restart=always

Try it This post is a practical reference for the dev who's tired of Dockerfile + docker-compose.yml + nginx config + Watchtower for a personal project. Everything below works on a stock Ubuntu/Debian VPS with no extra tooling beyond python3 and pip. I run Funding Finder on this exact setup: 5 systemd services, 1 Python interpreter, ~70 MB resident memory total, $5/month VPS. Up for over 24 hours with zero issues at the time of writing. The 5 unit files are: API server, background collector loop, healthcheck monitor, alerts worker, mission supervision dashboard. They're all independent processes managed by systemd's Restart=always, log to disk, survive reboots, and start automatically on boot. Copy-paste each into /etc/systemd/system/<name>.service and run systemctl enable --now <name> to activate. The Flask dev server is fine for personal projects up to ~500 req/sec on a single core. If you need more, swap ExecStart for gunicorn -w 4 -b 0.0.0.0:8083 api:app. Same unit file, one line different. The collector itself runs while True: collect_all(); time.sleep(300). systemd's Restart=always handles process crashes — if Python segfaults or hits an unhandled exception that escapes the loop, systemd brings it back in 10 seconds. For a slower cycle (say, 10 minutes), change --loop 300 to --loop 600 and systemctl daemon-reload && systemctl restart funding-finder-collector. No code change needed. The Wants= directive means: if the API service is stopped, this monitor will be too (but won't fail). The After= directive ensures the monitor starts after the API on boot, so it doesn't immediately alert about a "missing" API that's still booting. The monitor itself polls /api/health every 10 minutes and sends a Telegram message after 2 consecutive failures. ~150 lines of Python. This is the user-facing "alerts" feature that the paid tier uses. 60-second polling cycle, scans the database for active alerts, sends matching Telegram messages with cooldown enforcement. ~200 lines of Python. I wrote a separate post on the architecture of this worker — short version, you don't need a queue or websockets at this scale. A second Flask service for a different concern (mission supervision dashboard). Same template, different working directory and port. Save the 5 files above into /etc/systemd/system/, then: Five commands. Done. All five services are running, will restart on crash, will start on boot, and are logging to files you can tail -f. To restart everything (e.g. after a code change): To stop everything (e.g. for maintenance): Each of those things makes sense at different scales: Until each of those triggers fires, the boring stack is the right answer. Stop pre-optimizing for problems you don't have. This is the magic line. Without it, a Python crash means the service stays dead until you SSH in and notice. With it, you get automatic recovery from 95% of failure modes for free. The other 5% (corrupted database, full disk, etc) require you to actually fix the underlying problem, but for "the process died because of an unhandled exception" or "OOM killer reaped it", systemd's restart is enough. Combined with RestartSec=5 (or 10, or 30 — pick based on the cost of a fast restart loop), you get a deployment that's harder to break than 90% of the Kubernetes setups I've seen. If you're running a personal project on a Docker Compose stack right now and feeling the operational weight of it, try this exercise: rewrite ONE of your services as a systemd unit. It's a 1-hour project. You'll either love it or you'll discover you actually need the Docker isolation for some specific reason. Most of the time, you'll love it. The full source for the 5 unit files above is in the Funding Finder repo (which is the project they run). Live tool that uses them: http://178.104.60.252:8083/ Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

[Unit] Description=Funding Finder API server After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/api.py Restart=always RestartSec=5 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/api.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/api.log Environment=PORT=8083 [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder API server After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/api.py Restart=always RestartSec=5 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/api.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/api.log Environment=PORT=8083 [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder API server After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/api.py Restart=always RestartSec=5 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/api.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/api.log Environment=PORT=8083 [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder data collector (5-min loop) After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/collector.py --exchanges binance,bybit,okx,bitget,mexc,hyperliquid,gateio,dydx --loop 300 Restart=always RestartSec=10 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/collector.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/collector.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder data collector (5-min loop) After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/collector.py --exchanges binance,bybit,okx,bitget,mexc,hyperliquid,gateio,dydx --loop 300 Restart=always RestartSec=10 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/collector.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/collector.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder data collector (5-min loop) After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/collector.py --exchanges binance,bybit,okx,bitget,mexc,hyperliquid,gateio,dydx --loop 300 Restart=always RestartSec=10 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/collector.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/collector.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder healthcheck monitor (Telegram alerts) After=network.target funding-finder-api.service Wants=funding-finder-api.service [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/monitor.py --interval 600 --alert-after 2 Restart=always RestartSec=30 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/monitor.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/monitor.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder healthcheck monitor (Telegram alerts) After=network.target funding-finder-api.service Wants=funding-finder-api.service [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/monitor.py --interval 600 --alert-after 2 Restart=always RestartSec=30 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/monitor.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/monitor.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder healthcheck monitor (Telegram alerts) After=network.target funding-finder-api.service Wants=funding-finder-api.service [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/monitor.py --interval 600 --alert-after 2 Restart=always RestartSec=30 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/monitor.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/monitor.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder alerts worker (Telegram dispatch) After=network.target funding-finder-api.service Wants=funding-finder-api.service [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/alerts_worker.py --interval 60 Restart=always RestartSec=15 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/alerts.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/alerts.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder alerts worker (Telegram dispatch) After=network.target funding-finder-api.service Wants=funding-finder-api.service [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/alerts_worker.py --interval 60 Restart=always RestartSec=15 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/alerts.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/alerts.log [Install] WantedBy=multi-user.target [Unit] Description=Funding Finder alerts worker (Telegram dispatch) After=network.target funding-finder-api.service Wants=funding-finder-api.service [Service] Type=simple WorkingDirectory=/root/project_30d/artifacts/funding_finder ExecStart=/usr/bin/python3 /root/project_30d/artifacts/funding_finder/alerts_worker.py --interval 60 Restart=always RestartSec=15 StandardOutput=append:/root/project_30d/artifacts/funding_finder/data/alerts.log StandardError=append:/root/project_30d/artifacts/funding_finder/data/alerts.log [Install] WantedBy=multi-user.target [Unit] Description=Mission 30j supervision dashboard After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d ExecStart=/usr/bin/python3 /root/project_30d/dashboard/backend.py Restart=always RestartSec=5 StandardOutput=append:/root/project_30d/journal/backend.log StandardError=append:/root/project_30d/journal/backend.log Environment=PORT=8082 [Install] WantedBy=multi-user.target [Unit] Description=Mission 30j supervision dashboard After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d ExecStart=/usr/bin/python3 /root/project_30d/dashboard/backend.py Restart=always RestartSec=5 StandardOutput=append:/root/project_30d/journal/backend.log StandardError=append:/root/project_30d/journal/backend.log Environment=PORT=8082 [Install] WantedBy=multi-user.target [Unit] Description=Mission 30j supervision dashboard After=network.target [Service] Type=simple WorkingDirectory=/root/project_30d ExecStart=/usr/bin/python3 /root/project_30d/dashboard/backend.py Restart=always RestartSec=5 StandardOutput=append:/root/project_30d/journal/backend.log StandardError=append:/root/project_30d/journal/backend.log Environment=PORT=8082 [Install] WantedBy=multi-user.target sudo systemctl daemon-reload sudo systemctl enable --now funding-finder-api sudo systemctl enable --now funding-finder-collector sudo systemctl enable --now funding-finder-monitor sudo systemctl enable --now funding-finder-alerts sudo systemctl enable --now project30d-dashboard sudo systemctl daemon-reload sudo systemctl enable --now funding-finder-api sudo systemctl enable --now funding-finder-collector sudo systemctl enable --now funding-finder-monitor sudo systemctl enable --now funding-finder-alerts sudo systemctl enable --now project30d-dashboard sudo systemctl daemon-reload sudo systemctl enable --now funding-finder-api sudo systemctl enable --now funding-finder-collector sudo systemctl enable --now funding-finder-monitor sudo systemctl enable --now funding-finder-alerts sudo systemctl enable --now project30d-dashboard systemctl is-active funding-finder-{api,collector,monitor,alerts} project30d-dashboard systemctl is-active funding-finder-{api,collector,monitor,alerts} project30d-dashboard systemctl is-active funding-finder-{api,collector,monitor,alerts} project30d-dashboard sudo systemctl restart funding-finder-{api,collector,monitor,alerts} sudo systemctl restart funding-finder-{api,collector,monitor,alerts} sudo systemctl restart funding-finder-{api,collector,monitor,alerts} sudo systemctl stop funding-finder-{api,collector,monitor,alerts} sudo systemctl stop funding-finder-{api,collector,monitor,alerts} sudo systemctl stop funding-finder-{api,collector,monitor,alerts} - No Dockerfile — your code runs in the host Python, no isolation layer - No docker-compose.yml — systemd is the orchestrator - No nginx in front — Flask listens on the public port directly (or use ufw to control which IPs can connect) - No process manager (supervisor, pm2, etc) — systemd is the process manager - No log shipper (filebeat, fluentd, etc) — tail is enough at small scale - No image registry — there are no images - No CI/CD pipeline — git pull && systemctl restart is your deploy - No staging environment — fix it in prod, you have an audience of 0 right now - No "production-grade" web server — Flask dev server handles ~500 req/sec on 1 core, plenty for personal projects - Docker when you need to ship the same app to multiple environments with different OS - nginx when you need TLS termination, rate limiting, or static file caching at the edge - gunicorn when you exceed ~500 req/sec sustained - A real DB when SQLite WAL can't handle the write contention (rarely below 50k writes/sec) - A staging environment when you have paying customers whose downtime would cost real money - A CI pipeline when there's more than one developer