Automated Deploy Stack
End-to-end DevOps pipeline — Terraform provisions the VPS, Ansible configures it, GitHub Actions builds and ships containers, Prometheus and Grafana watch everything.
- devops
- terraform
- ansible
- docker
- github-actions
- prometheus
- grafana
A full deployment pipeline built from scratch on a self-managed VPS. No managed CI, no cloud databases, no hidden scaffolding — every layer is visible and version-controlled.
What it does
Push to main → tests run → Docker image builds and is pushed to the registry →
the VPS pulls the new image and hot-swaps the container with zero downtime.
Prometheus scrapes metrics on every deploy; Grafana dashboards show request rate,
error rate, and latency in real time.
Stack
| Layer | Tool | Why |
|---|---|---|
| Provisioning | Terraform + Hetzner Cloud | reproducible infra, teardown in one command |
| Configuration | Ansible | idempotent server setup, firewall rules, user accounts |
| Containers | Docker + Compose | portable, same image runs in CI and prod |
| CI/CD | GitHub Actions | build → test → push → deploy in a single workflow |
| Reverse proxy | Caddy | automatic TLS, zero config |
| Metrics | Prometheus + Node Exporter | scrapes host and app metrics every 15 s |
| Dashboards | Grafana | RED method dashboard (requests, errors, duration) |
| Alerting | Alertmanager → Telegram | pings me when error rate exceeds 1 % |
How the pipeline works
┌──────────────┐ push ┌───────────────────┐
│ local git │ ──────────► │ GitHub Actions │
└──────────────┘ │ │
│ 1. vitest │
│ 2. docker build │
│ 3. ghcr.io push │
│ 4. ssh deploy.sh │
└─────────┬─────────┘
│ SSH
▼
┌───────────────────┐
│ VPS (Hetzner) │
│ │
│ docker compose │
│ pull + up -d │
│ │
│ Caddy → app │
│ Prometheus scrape │
└───────────────────┘
The deploy script waits for the health endpoint to return 200 before removing
the old container, giving a clean rollover without dropped requests.
Infrastructure as code
The entire server is reproducible from two commands:
terraform apply # provisions the VPS, DNS record, firewall rules
ansible-playbook site.yml # installs Docker, Caddy, creates deploy userTearing the whole thing down and rebuilding it costs almost nothing — which makes it easy to test the provisioning scripts themselves.
What I learned
- Secrets management: GitHub Actions secrets feed into the Ansible vault and the Compose env file — nothing sensitive touches the repo.
- Idempotency matters: running the Ansible playbook twice should change nothing. Getting that right forced me to understand state.
- Observability first: adding Prometheus before the app was "done" meant every bug showed up as a spike in the dashboard, not a mystery in the logs.
- SSH hardening: key-only auth, non-standard port, fail2ban — small steps that matter when the server is public.