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, 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.
Prerequisites
Section titled “Prerequisites”A k3s/Kubernetes cluster with these operators installed:
- ArgoCD
- the CloudNativePG operator
- cert-manager + a DNS-01-capable
ClusterIssuerwhose credential covers your zone - 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 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.
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 site serves the apex (<domain>), the app servesapp.<domain>, and GlitchTip servesglitchtip.<domain>. Set the production site values inoverlays/prod/site-build.envtoo (see Public site). - the
ClusterIssuername inoverlays/prod/certificate.yaml(its credential must cover every domain indnsNames) - 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 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.
4. Verify
Section titled “4. Verify”kubectl -n boringstack-prod get pods # all Running/Readycurl -sI https://app.<domain>/health # 200 from the apicurl -sI https://app.<domain>/ # 200 from the ui (SPA)curl -sI https://<domain>/ # 200 from the public siteIn 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.
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.