Provisioning with k3s (GitOps)
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.
Prerequisites
Section titled “Prerequisites”A k3s/Kubernetes cluster with these operators installed:
- ArgoCD + argocd-image-updater (with its
argocd/image-updater-git-credswrite-back secret) - the CloudNativePG operator
- cert-manager + a DNS-01-capable
ClusterIssuer - Traefik (ships with k3s) exposing
web+websecureentrypoints - 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.
1. Rebrand and set the knobs
Section titled “1. Rebrand and set the knobs”From your fork’s root:
./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
ClusterIssuername inoverlays/prod/certificate.yaml - the
StorageClassinbase/postgres/cluster.yamlandbase/valkey/statefulset.yaml(optional; defaults to the cluster default) source.repoURLinargocd/boringstack-prod.yaml
2. Seed secrets
Section titled “2. Seed secrets”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).
3. Register the app with ArgoCD
Section titled “3. Register the app with ArgoCD”Either apply the Application directly:
kubectl apply -f infra/k3s/argocd/boringstack-prod.yamlOr, 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.
How it works
Section titled “How it works”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.
4. Verify
Section titled “4. Verify”kubectl -n boringstack-prod get pods # all Running/Readycurl -sI https://<domain>/health # 200 from the apicurl -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.
First-deploy checks
Section titled “First-deploy checks”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 theglitchtip-webpod reach Ready, then log in once atglitchtip.<domain>. - The
glitchtipdatabase is created by the CloudNativePG cluster’spostInitSQL, which runs only at initial bootstrap. If you point GlitchTip at an already-initialized cluster, create the database manually (CREATE DATABASE glitchtip OWNER boringstack;).
What stays manual
Section titled “What stays manual”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.