Stack
What runs my personal infrastructure, with the reasoning behind each choice. And, more usefully, what I ruled out: a tool you picked tells you nothing until you know what it beat.
This page describes my personal infrastructure, the one where every decision is mine. The tools I operate at work — Active Directory, CrowdStrike, Fortinet, Proxmox, Prometheus/Grafana — come with constraints that are not mine to set; they are covered in the dedicated case study.
language
-
Go
All three applications are single Go binaries, compiled with
CGO_ENABLED=0. A static binary, no interpreter, no package manager at runtime: the final image contains one file and nothing else. On a 1.8 GB cluster that is not an aesthetic preference, it is a memory budget.Ruled out: Python and Node. Both are excellent for iterating, and both bring a runtime, a dependency tree to maintain, and a vulnerability surface with a life of its own. For software I need to leave running for months without touching it, the trade-off leans clearly one way.
-
The standard library, almost alone
No web framework, no ORM:
net/httpanddatabase/sqlare enough. The Frigo project's only direct dependencies are a SQLite driver and a Web Push library — everything else is stdlib.Ruled out: Gin, Echo, GORM. They save time at the start and cost it at every version bump. On a project where I am the sole maintainer, fewer dependencies means fewer things breaking while I am looking elsewhere.
data
-
SQLite via
modernc.org/sqliteOne database per household, a single file on a persistent volume. The driver is SQLite transpiled to pure Go: no CGO, so static compilation stays possible and the final image carries no C library.
Ruled out:
mattn/go-sqlite3, which is faster but requires CGO — hence a cross-compilation toolchain, a non-empty base image, and giving up the static binary. The performance gain was irrelevant at this scale.Accepted consequence: a single writer. Hence
SetMaxOpenConns(1), one replica, and aRecreatedeployment strategy — so a few seconds of downtime on every update. Why I did not "fix" it: the note describing the diagnosis (in French). -
MariaDB for Coach Adaptatif
A dedicated instance, isolated by NetworkPolicy, backed up by a daily CronJob with checksums. Its data model is genuinely relational and concurrent — the case where SQLite stops being the right tool.
Ruled out: sharing one database between both applications. It would have saved memory, at the cost of coupling two unrelated projects and sharing their outages.
platform
-
k3s
Single-node Kubernetes on a Debian VPS. A trimmed distribution: etcd replaced by SQLite, Traefik and the Helm controller built into the binary — so 0 MB resident for the latter.
Ruled out:
kubeadm, which did not fit the memory envelope. And docker-compose, which I used before: perfectly adequate for running containers, but with no state reconciliation, no declarative rollback, no NetworkPolicies. -
Flux CD
GitOps in pull mode: the cluster pulls its state from a Git repository and reconciles continuously. The pipeline holds no SSH or kubectl access to the machine; a rollback is a
git revert.Ruled out: Argo CD — more capable but noticeably hungrier, and its web UI would be one more exposed surface. Also ruled out: deploying by push from CI, which would hand a third party a permanent path into production. That is the same reasoning behind the cluster figures on the infrastructure case study being measured by hand.
-
SOPS + age
Secrets are versioned in Git, encrypted, and decrypted only inside the cluster by a key that exists nowhere else. A secret's diff stays readable in structure: you can see which key changed, never its value.
Ruled out: HashiCorp Vault — sized for organisations, not a single node. Sealed Secrets, which does not let you read back what you encrypted. And above all: keeping secrets outside Git, which amounts to no longer knowing what is actually deployed.
-
Traefik + cert-manager
Ingress provided by k3s, Let's Encrypt certificates renewed automatically. The Kubernetes API on port 6443 is not exposed: administration goes through an SSH tunnel.
hardening
-
Containers constrained by default
Every pod runs
runAsNonRoot(UID 65532), read-only root filesystem,seccompProfile: RuntimeDefault, capabilities dropped, service-account token not mounted. Images are pinned by digest and a Trivy scan blocks CI.The principle: these settings are not added after an incident, they are the starting point. Kubernetes, by contrast, allows everything by default — including pod-to-pod traffic, hence a NetworkPolicy in front of every database.
-
This site
Unprivileged nginx serving static files.
default-src 'none', the single inline script allowed by its SHA-256 hash, HSTS with preload, asecurity.txtconforming to RFC 9116. Zero cookies, zero trackers, zero third-party requests — including fonts, which are the system's own.Ruled out: any static site generator. Hand-written HTML pages, one stylesheet, one optional JavaScript file. Adding Hugo or Astro would introduce a build chain for a problem I do not have. Also ruled out: analytics, a CDN, and a manual theme toggle — reasoning here (in French).
what I deliberately don't use
-
No monitoring on this cluster
No Prometheus, no Grafana here: the full stack would cost more RAM than the three applications combined. I rely on Kubernetes probes, logs and manual checks. That is a trade-off, not a best practice — on the corporate infrastructure, where the memory constraint does not exist, monitoring is in place.
-
No high availability
One node, one replica per application. A hardware failure means an outage. The choice is budgetary and deliberate: better a single node whose every component I understand than a pretence of HA whose real cost I could not pay.
The decisions told in full — including the wrong diagnoses — are in the technical notes, written in French.