Skip to content
Gabriel.
← All work

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.

Visit project ↗
  • 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

LayerToolWhy
ProvisioningTerraform + Hetzner Cloudreproducible infra, teardown in one command
ConfigurationAnsibleidempotent server setup, firewall rules, user accounts
ContainersDocker + Composeportable, same image runs in CI and prod
CI/CDGitHub Actionsbuild → test → push → deploy in a single workflow
Reverse proxyCaddyautomatic TLS, zero config
MetricsPrometheus + Node Exporterscrapes host and app metrics every 15 s
DashboardsGrafanaRED method dashboard (requests, errors, duration)
AlertingAlertmanager → Telegrampings 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 user

Tearing 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.