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, CI publishes GHCR images, argocd-image-updater bumps the tag, and ArgoCD syncs. HA, autoscaling, and TLS come with it.

Source lives in infra/k3s.

A k3s/Kubernetes cluster with these operators installed:

  • ArgoCD + argocd-image-updater (with its argocd/image-updater-git-creds write-back secret)
  • the CloudNativePG operator
  • cert-manager + a DNS-01-capable ClusterIssuer
  • 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 CI that builds and pushes ghcr.io/<owner>/<project>-api and -ui images, the same pipeline the Compose/WUD path uses.

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 ClusterIssuer name in overlays/prod/certificate.yaml
  • 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 builds + pushes<br/>GHCR images"]
  iu["argocd-image-updater<br/>bumps kustomization tag"]
  argo["ArgoCD syncs<br/>overlays/prod"]
  presync["PreSync Job<br/>db:migrate + db:seed"]
  k8s["Rolling deploy<br/>api · ui · GlitchTip"]

  push --> ci --> iu --> argo --> presync --> k8s

ArgoCD renders the prod overlay (base + secrets component + HA patches), runs the PreSync migration Job, then rolls out the Deployments. 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://<domain>/health # 200 from the api
curl -sI https://<domain>/ # 200 from the ui (SPA)

In ArgoCD the Application should be Synced and Healthy, and a new commit’s image should auto-roll within an image-updater poll interval.

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.