ADR-0013 — Run MVP-1 on a single VPS via docker-compose
Status
Section titled “Status”Accepted — 2026-04-28
Context
Section titled “Context”MVP-1 traffic is < 100 messages/day from two known users. The bot must be reachable from Telegram (long-polling — ADR-0012 — so no inbound HTTPS endpoint required) and store data in PostgreSQL with local Prometheus + Grafana.
The operator is a solo developer who is also the primary user. There is no on-call rotation, no SLO commitment beyond “best effort”, and no compliance requirement.
Considered alternatives
Section titled “Considered alternatives”| Option | Summary | Pros | Cons | Outcome |
|---|---|---|---|---|
| A — Managed Kubernetes (GKE / EKS) | Each service as a Deployment | ”Production-grade”; trivial scale-out | Months of ops setup; ~$70/mo idle cost; comically over-engineered for 100 msg/day; no scale story justifies it | rejected |
| B — Self-hosted k3s on the VPS | k3s + Helm chart per service | Lighter than full k8s; Helm portfolio sample | Still wildly more YAML than two docker-compose services; troubleshooting eats focus | rejected |
| C — Managed services (Heroku / Fly / Render) | Each container as a managed dyno | Zero ops | $$ at idle; Postgres add-ons cost; vendor lock-in for a bot that should be trivially portable | rejected |
| D (chosen) — Single VPS + docker-compose | One $5–10/mo VPS, all containers via docker compose up | Simplest possible; portable across providers; all logs/metrics local | Single point of failure (unimportant for personal bot); manual scale-up later if needed | selected |
Decision
Section titled “Decision”We will host the entire MVP-1 stack on one VPS managed by
docker-compose. The compose file (compose.yml) defines four
services: bot, postgres, prometheus, grafana. Volumes are
named (not bind-mounts) so backup/restore is docker run --rm -v pgdata:/data ... tar-style.
All host ports bind to 127.0.0.1 only. External access is via SSH
tunnel for Grafana and via Telegram itself for the bot. No reverse
proxy, no public TLS, no firewall rules beyond default-deny inbound.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Reproducible local dev: same
compose.ymlon laptop and on VPS. - Two-line deploy:
docker compose pull && docker compose up -d. - ~$10/mo all-in (VPS only).
Negative / trade-offs
Section titled “Negative / trade-offs”- VPS reboot kills the service for ~30 s. Acceptable: Telegram redelivers updates.
- No automatic horizontal scaling. Re-evaluate at MVP-2 if user count grows past ~50.
- Single-VPS backup story is “rsync
pgdatavolume to S3 nightly” — basic but sufficient. Will be formalized in Plan 8 deploy task.
Neutral / follow-ups
Section titled “Neutral / follow-ups”- Plan 8: write a
make deployMakefile target that doesdocker compose pull && up -d --remove-orphans. - Plan 8: add nightly
pg_dumpcron + S3 upload. - Re-evaluate at MVP-2 when external onboarding starts (potential trigger to move to a small managed Postgres for backup off-host).
References
Section titled “References”- ADR-0004 (Modular monolith) — only one bot container is needed.
- ADR-0012 (Long-polling) — no inbound HTTPS required.
- compose.yml — current deployment definition.