k3s GitOps infrastructure
The platform serving the page you are reading — Kubernetes in production on a 1.8 GB VPS.
the problem
One VPS, several applications to host (this site, Coach Adaptatif, more to come), and the wish for a real pipeline: automatic deployment, staging, one-command rollback. The accepted constraint: 1.8 GB of RAM — where most Kubernetes guides recommend 4 GB as a minimum.
architecture
Internet → Traefik (ingress) + cert-manager (Let's Encrypt) ├── jjoly.eu → static site (nginx, this site) ├── coach.jjoly.eu → coach-app + dedicated MariaDB ├── fridge.jjoly.eu → fridge-app + SQLite (one database per household) └── staging.* → staging namespace (0 replicas when idle) Flux CD watches the infrastructure Git repo → applies every change CI (GitHub Actions): tests → build → Trivy scan → GHCR → commit the tag
the choices that matter
- GitOps pull, not push: the cluster pulls its state from Git.
No SSH access in the pipeline, rollback =
git revert. - Ruthless RAM budget: Flux trimmed to 2 controllers, Helm charts through the controller built into k3s (0 MB resident), no heavy monitoring stack, ephemeral staging.
- Secrets encrypted in Git (SOPS + age): auditable, versioned, decrypted only inside the cluster.
- NetworkPolicies everywhere: each database is reachable only by its own application — by default, Kubernetes lets everything through.
- The k8s API is never exposed: port 6443 closed to the public, administration over an SSH tunnel only.
- Workload hardening: read-only, non-root containers, dropped capabilities, images pinned by digest, vulnerability scanning that blocks CI.
resilience
- Daily MariaDB backups (CronJob), 7-day + 4-week retention, checksummed.
- Migration off the previous docker-compose stack carried out with a double safety net (encrypted dump + off-server copy) and rollback available throughout.
- Extensible to multiple nodes without a rewrite:
k3s agent+ labels; stateless apps spread out, stateful data stays pinned.
the result
A push to main updates staging in about a minute; a
vX.Y.Z tag deploys to production. Adding an application means
duplicating a folder of manifests. This site is the living demonstration.
cluster state
- 1430 MB of RAM in use out of 1870 (76%)
- 15 running pods on 1 node
Measured on 2026-07-29 with outils/mesurer-infra.sh. These figures are not collected by CI: granting the runner cluster access would open a path to production from a third party.
what runs on it
| Service | Version in production | At last build |
|---|---|---|
| Frigo Adaptatif | v3.9.0 |
returned 200 |
| Coach Adaptatif | v0.1.0 |
returned 200 |
| jjoly.eu | sha-af5a4b3 |
returned 200 |
The technical notes behind these choices — including the debugging mistakes — are written in French.