GitHub Actions + Hetzner Cloud + Kubernetes = winning combo for cost-controlled B2B SaaS in 2026. AWS EKS costs $70/month minimum for nothing. Hetzner K8s: €8-30/month real cluster. Here's the complete architecture.
TL;DR
- GitHub Actions: CI tests + build + auto deploy.
- Hetzner K8s or Talos: managed or self-managed cluster.
- ArgoCD: GitOps for deploys.
- Total stack: €80-200/month for medium-scale SaaS.
Complete architecture
`
[GitHub Repo]
↓
[GitHub Actions]
├── Test (Vitest, Playwright)
├── Lint (ESLint, Prettier)
├── Build (Next.js, Docker)
├── Push image (GitHub Container Registry)
└── Update GitOps repo
↓
[ArgoCD watches GitOps repo]
↓
[Hetzner K8s cluster]
├── Frontend Next.js
├── Backend API
├── Postgres (Neon or self-host)
└── Redis
`
Step 1 — CI pipeline
`yaml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Type-check
run: pnpm tsc --noEmit
- name: Lint
run: pnpm lint
- name: Unit tests
run: pnpm test
- name: E2E tests
run: pnpm playwright test
- name: Cross-tenant security tests
run: pnpm test:tenant-scoping
build-and-push:
if: github.ref == 'refs/heads/main'
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker
uses: docker/build-push-action@v5
with:
push: true
tags: |
ghcr.io/kolonell/app:${{ github.sha }}
ghcr.io/kolonell/app:latest
- name: Update GitOps manifest
run: |
git clone https://github.com/kolonell/k8s-manifests
cd k8s-manifests
sed -i "s|image: ghcr.io/kolonell/app:.*|image: ghcr.io/kolonell/app:${{ github.sha }}|" prod/deployment.yaml
git commit -am "deploy: ${{ github.sha }}"
git push
`
Step 2 — multi-stage Dockerfile
`dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
USER node
CMD ["node", "server.js"]
`
Final image: ~150 MB (vs 800 MB without multi-stage).
Step 3 — Hetzner K8s cluster
Option A — Hetzner managed K8s (beginner-recommended)
`bash
# Create via hetzner.com console (managed Kubernetes)
# Cost: €30/month for control plane + €8-15/node
`
Option B — Self-managed with k3s
More economical:
`bash
# On Hetzner CX21 master node (4 vCPU, 8 GB)
curl -sfL https://get.k3s.io | sh -
sudo cat /etc/rancher/k3s/k3s.yaml
curl -sfL https://get.k3s.io | K3S_URL=https://MASTER_IP:6443 K3S_TOKEN=TOKEN sh -
`
3-node cluster: €24-45/month total.
Step 4 — K8s manifests
`yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: prod
spec:
replicas: 3
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
containers:
- name: app
image: ghcr.io/kolonell/app:abc123
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: app
namespace: prod
spec:
selector:
app: app
ports:
- port: 80
targetPort: 3000
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts: [app.kolonell.com]
secretName: app-tls
rules:
- host: app.kolonell.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port: { number: 80 }
`
Step 5 — ArgoCD GitOps
`bash
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
`
`yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-prod
spec:
project: default
source:
repoURL: https://github.com/kolonell/k8s-manifests
targetRevision: HEAD
path: prod
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true
selfHeal: true
`
ArgoCD watches K8s manifests repo + auto-syncs on every push.
Step 6 — secrets management (Sealed Secrets)
`bash
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
kubectl create secret generic app-secrets \
--from-literal=database-url='postgres://...' \
--dry-run=client -o yaml > secret.yaml
kubeseal -o yaml < secret.yaml > sealed-secret.yaml
# Commit sealed-secret.yaml to Git (encrypted)
`
Encrypted Git secrets, only decryptable by the cluster.
Step 7 — K8s observability
`bash
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace
`
Included dashboards: pods CPU/RAM, latency, error rate, request volume.
Typical monthly costs
| Component | Spec | Cost |
|---|---|---|
| Hetzner K8s control plane | Managed | €30/month |
| Worker nodes (3× CX21) | 4 vCPU 8 GB each | €24/month |
| Postgres (Neon Pro) | 5 GB | €19/month |
| Redis (Upstash) | Pay-as-you-go | €5-15/month |
| Cloudflare CDN | Free + Pro | €0-25/month |
| Sentry | 50K errors | €26/month |
| GitHub Actions | 3K minutes | €0 (free tier) |
| Monthly total | ~€110-160 |
Compare AWS EKS: ~$300-500/month for equivalent. 60-70% savings.
Real case — Dakar SaaS (350 tenants, 1.2M req/day)
| Metric | Stack |
|---|---|
| Cluster | Hetzner K8s 5 nodes |
| Monthly infra cost | €145 |
| Deploy frequency | 12-18/day |
| Deploy duration | 4-7 min |
| Rollback time | 30 sec (ArgoCD) |
| Uptime | 99.96% |
Common pitfalls
- No readiness probe — pods receive traffic before being ready.
- Missing resources requests/limits — random OOM kills.
- Plaintext secrets in Git — catastrophic leak. Sealed Secrets mandatory.
- No rolling update strategy — downtime on every deploy.
- No K8s state backup — Velero or similar for DR.
FAQ
Q: K8s vs Vercel for Next.js?
A: Vercel = simple but expensive at scale + lock-in. K8s = complex but unlimited economic scaling + portable.
Q: k3s vs k0s vs RKE?
A: k3s = simplest + battle-tested. k0s = modern alternative. RKE = SUSE enterprise.
Q: Multi-region multi-cluster?
A: For 99% Africa B2B SaaS: single Frankfurt cluster enough. Multi-region useful past $100M ARR.
Conclusion
CI/CD GitHub Actions + Hetzner K8s + ArgoCD = pro 2026 DevOps stack for serious B2B SaaS at reasonable cost. 2-4 weeks initial setup. ROI: dev agility + independent scaling + zero cloud lock-in.
Mohamed Bah
Fondateur, Kolonell
Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.