marcio.cloud
All projects

Containers

DevLink — URL Shortener & Analytics on k3s + Hetzner

A microservices URL shortener with click analytics, running on a single-node k3s cluster on Hetzner Cloud — provisioned with Terraform, configured with Ansible, fronted by Traefik with auto-issued Let's Encrypt TLS.

Kubernetes (k3s)TerraformAnsibleDockerTraefikcert-managerNode.jsPython / FastAPIReactPostgreSQLRedis StreamsGitHub ActionsHetzner Cloud
Architecture diagram for DevLink — URL Shortener & Analytics on k3s + Hetzner
Architecture diagram — created in Lucidchart.

Problem

Most portfolio-grade k8s demos either run a single nginx pod on minikube or burn $50/month on a managed EKS control plane. I wanted to prove I could ship a real multi-service application — with async events, persistent data and HTTPS — on a production-shaped cluster that still costs less than a cup of coffee a month.

Solution

A four-service URL shortener (auth, url, analytics, frontend) deployed on a single-node k3s cluster running on a low-cost Hetzner VPS. The VPS is provisioned end-to-end with Terraform using the hcloud provider; Ansible roles (base, docker, k3s) configure the host and install the cluster. Traefik (included with k3s) handles ingress and cert-manager negotiates Let's Encrypt certificates automatically. The url-service publishes click events to a Redis Stream which the Python analytics-service consumes asynchronously — keeping the redirect path fire-and-forget. GitHub Actions builds multi-stage, non-root Docker images and pushes them to Docker Hub on every merge to main.

Architecture

  • Terraform (hcloud provider) provisions a single cpx21 VPS — `terraform apply` recreates the whole stack in ~60 seconds.
  • Ansible roles install Docker and k3s, then copy kubeconfig back to the workstation.
  • Four services run as Deployments with 2 replicas each: auth-service (Node.js + JWT), url-service (Node.js), analytics-service (Python + FastAPI), frontend (React + Vite served via Nginx).
  • Redis Streams decouple the click-write path from analytics aggregation — the redirect endpoint never blocks on analytics.
  • PostgreSQL 16 backs both auth and url services; Redis 7 doubles as event bus and analytics cache.
  • Traefik ingress with cert-manager + Let's Encrypt issues and renews TLS automatically.
  • GitHub Actions builds multi-stage, non-root images and pushes to Docker Hub on push to main.

Outcomes

  • Total monthly infrastructure cost ≈ €5.50 — VPS, domain and CI all in.
  • Rolling updates with zero downtime across 2 replicas per service.
  • End-to-end reproducibility — destroy the VPS, run `terraform apply` + `ansible-playbook` + `kubectl apply`, and the stack is back in under 5 minutes.
  • HTTPS automated via cert-manager — no manual cert renewal, ever.

What I'd do differently

Most of the value of 'k8s on a hyperscaler' disappears when you only need a single node — k3s on a cheap VPS gives you the same kubectl workflow at a fraction of the cost. Redis Streams is a sweet spot between 'just call the analytics service directly' and 'spin up Kafka' — it costs you one container and buys you real decoupling. And the Terraform + Ansible split (provision vs. configure) is worth the extra moving part: when something drifts, you know exactly which tool owns the fix.