Skip to content
BoringStack
Star

Infra template: Kubernetes (k3s)

3 min read

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.

WorkloadManifestNotes
apibase/api/Bun/Elysia, port 7330, /health (liveness) and /ready (readiness)
uibase/ui/nginx static SPA, port 8080
sitebase/site/Public marketing site (apps/site), nginx static, port 8080, on the apex domain
Postgresbase/postgres/CloudNativePG Cluster, 1 instance (3 in prod)
Valkeybase/valkey/in-namespace cache and BullMQ backend
GlitchTipoverlays/prod/glitchtip/per-project error tracking (web and worker)
migrationsbase/api/migration-job.yamlArgoCD 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 on app.<domain> goes to the ui (the SPA). The app keeps same-origin /api.

The two origins share no cookies. See Public site.

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 | plain

base/ 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.

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.

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 kustomize secretGenerator over a gitignored secret.env.

Switch by editing one line in overlays/prod/kustomization.yaml. See the secrets backends runbook.

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.

Full knob list and walkthrough: Provisioning with k3s and infra/k3s/README.md.