Skip to content
BoringStack
Star

Provisioning with k3s (GitOps)

4 min read

The cluster deployment path. Where OpenTofu gives you a single VPS running docker-compose, this gives you a GitOps loop on a Kubernetes cluster you already run: push code, the release workflow publishes the API, migration, UI and site images as one set, and ArgoCD syncs that set. HA, autoscaling, and TLS come with it.

Source lives in infra/k3s.

A k3s/Kubernetes cluster with these operators installed:

  • ArgoCD
  • the CloudNativePG operator
  • cert-manager + a DNS-01-capable ClusterIssuer whose credential covers your zone
  • Traefik (ships with k3s) exposing web + websecure entrypoints
  • a default/persistent StorageClass
  • optional: kube-prometheus-stack (metrics and dashboards), and a secrets backend (Vault + VSO by default, or the SealedSecrets controller)

You’ll also need the release workflow (production-release.yml), which builds and digest-pins ghcr.io/<owner>/<project>-api, -migrations, -ui and -site. Set the repository variable PRODUCTION_RELEASE_MODE=coordinated once the cluster is ready. The production releases runbook has the activation steps.

From your fork’s root:

Terminal window
./scripts/rename-project.sh <project> <ghcr-owner> <domain>

That rewrites every boringstack* token across the repo, including the k3s manifests. Then edit the handful of cluster-specific values (all listed in infra/k3s/README.md):

  • the domain in overlays/prod/ingress.yaml, certificate.yaml, glitchtip/. The site serves the apex (<domain>), the app serves app.<domain>, and GlitchTip serves glitchtip.<domain>. Set the production site values in overlays/prod/site-build.env too (see Public site).
  • the ClusterIssuer name in overlays/prod/certificate.yaml (its credential must cover every domain in dnsNames)
  • the StorageClass in base/postgres/cluster.yaml and base/valkey/statefulset.yaml (optional; defaults to the cluster default)
  • source.repoURL in argocd/boringstack-prod.yaml

Pick a backend in overlays/prod/secrets/ (default Vault). For Vault, populate the KV-v2 paths and bind a role, as described in Secrets backends. The app’s secret keys are listed in infra/k3s/README.md. DATABASE_URL is injected by CloudNativePG, so it isn’t among them.

Add your fork to ArgoCD as a repo credential (an SSH deploy key for a private repo).

Either apply the Application directly:

Terminal window
kubectl apply -f infra/k3s/argocd/boringstack-prod.yaml

Or, if your cluster is an app-of-apps, drop infra/k3s/argocd/app-of-apps-registration.example.yaml into your app-of-apps repo. It points ArgoCD at your fork’s infra/k3s/argocd directory.

flowchart LR
  push["git push<br/>(app code)"]
  ci["CI passes for the commit"]
  build["Build api, migrations, ui, site<br/>as candidates"]
  pin["One commit pins<br/>all four digests"]
  argo["ArgoCD syncs<br/>overlays/prod"]
  mig["Sync hook, wave -1<br/>db:migrate only"]
  k8s["Rolling deploy<br/>api, then ui, site, GlitchTip"]

  push --> ci --> build --> pin --> argo --> mig --> k8s

ArgoCD renders the prod overlay (base + secrets component + HA patches) and syncs in wave order: infrastructure first, then the migration Job, then the API, then the UI and the site once the API is healthy. CloudNativePG manages Postgres, cert-manager issues the TLS cert, and the Vault Secrets Operator (or your chosen backend) materializes the app secrets.

Terminal window
kubectl -n boringstack-prod get pods # all Running/Ready
curl -sI https://app.<domain>/health # 200 from the api
curl -sI https://app.<domain>/ # 200 from the ui (SPA)
curl -sI https://<domain>/ # 200 from the public site

In ArgoCD the Application should be Synced and Healthy. Each promoted release should appear there as one sync, with the API, migration, UI and site digests changing together.

Two things only the first apply confirms on your cluster:

  • GlitchTip migrates its schema and seeds the superuser on first boot from the GLITCHTIP_* env, rather than from a separate Job. Watch the glitchtip-web pod reach Ready, then log in once at glitchtip.<domain>.
  • The glitchtip database is created by the CloudNativePG cluster’s postInitSQL, which runs only at initial bootstrap. If you point GlitchTip at an already-initialized cluster, create the database manually (CREATE DATABASE glitchtip OWNER boringstack;).

Provisioning the cluster itself: bring your own k3s. And in-cluster Prometheus/Grafana/Loki, since you integrate with the existing stack rather than deploy a second one.