Infra template: Kubernetes (k3s)
infra/k3s is the cluster deployment target, the alternative to the single-host
infra/compose and OpenTofu/Hetzner
paths. Pick the one that fits your operating model; you don’t need all three.
It ships BoringStack to any ArgoCD-managed k3s/Kubernetes cluster as GitOps: push code, one release workflow builds the API, migration and UI images and pins their digests together, and ArgoCD syncs that set. The whole app runs in one namespace with HA, autoscaling, and TLS.
BoringStack is Compose-first on purpose, so this target lives in its own
infra/k3s/ subtree and is entirely opt-in. It never sits in the default deploy
path.
What runs
Section titled “What runs”| Workload | Manifest | Notes |
|---|---|---|
| api | base/api/ | Bun/Elysia, port 7330, /health (liveness) and /ready (readiness) |
| ui | base/ui/ | nginx static SPA, port 8080 |
| site | base/site/ | Public marketing site (apps/site), nginx static, port 8080, on the apex domain |
| Postgres | base/postgres/ | CloudNativePG Cluster, 1 instance (3 in prod) |
| Valkey | base/valkey/ | in-namespace cache and BullMQ backend |
| GlitchTip | overlays/prod/glitchtip/ | per-project error tracking (web and worker) |
| migrations | base/api/migration-job.yaml | ArgoCD Sync hook at wave -1: db:migrate only. Seeds are a deliberate one-off. |
Routing splits the public site from the app:
Host(<domain>)goes to the site, the static marketing site.Host(app.<domain>) && (/api or /health)goes to the api, and everything else onapp.<domain>goes to the ui (the SPA). The app keeps same-origin/api.
The two origins share no cookies. See Public site.
Layout
Section titled “Layout”infra/k3s/├── argocd/ ArgoCD Application and registration example├── base/ env-agnostic manifests: namespace, api, ui, valkey, postgres└── overlays/prod/ HA + TLS + secrets + GlitchTip + monitoring + patches └── secrets/ swappable backend: vault (default) | sealed | plainbase/ is generic. The prod overlay adds replicas and anti-affinity (api ×3,
ui ×2, site ×2, Postgres ×3), HPAs, PDBs, a ResourceQuota/LimitRange, Traefik middleware,
the TLS Certificate, GlitchTip, and the monitoring integration.
Prerequisites
Section titled “Prerequisites”The cluster must provide ArgoCD, the CloudNativePG operator, cert-manager with a
DNS-01 ClusterIssuer whose credential covers your zone, Traefik (k3s default), and
a persistent StorageClass. Optionally kube-prometheus-stack (for the
ServiceMonitor and Grafana dashboards) and your chosen secrets backend.
Secrets are pluggable
Section titled “Secrets are pluggable”Every workload reads two plain k8s Secrets, boringstack-secrets (app env) and
ghcr-registry-secret (GHCR pull), plus the CNPG-generated boringstack-db-app
that carries DATABASE_URL. The manifests never reference Vault directly, so how
those Secrets are produced is a swappable Kustomize component under
overlays/prod/secrets/:
vault(default): HashiCorp Vault via the Vault Secrets Operator.sealed: Bitnami SealedSecrets, encrypted and safe to commit.plain: a kustomizesecretGeneratorover a gitignoredsecret.env.
Switch by editing one line in overlays/prod/kustomization.yaml. See the
secrets backends runbook.
Monitoring
Section titled “Monitoring”The target does not deploy Prometheus/Grafana/Loki. It plugs into the cluster’s
kube-prometheus-stack via a ServiceMonitor that scrapes the api, plus optional
Grafana dashboard ConfigMaps. See overlays/prod/monitoring/README.md.
The prod overlay also adds business metrics: CloudNativePG runs aggregate queries over the sign-up, billing and audit tables, and a Grafana dashboard ConfigMap shows them. See Observability.
Get started
Section titled “Get started”Full knob list and walkthrough: Provisioning with k3s
and infra/k3s/README.md.
Related
Section titled “Related”- Provisioning with k3s; zero to live on a cluster.
- Production releases; the coordinated auto-deploy loop.
- Secrets backends (VSO / Sealed / plain).
- Postgres backups (CNPG).
- Infra overview; the Compose-first single-host target.