Kubernetes (Helm)
Deploy the Rhesis application onto a Kubernetes cluster using ArgoCD and the charts/rhesis
Helm chart (the same GitOps pattern, and the same ArgoCD bootstrap command, Rhesis uses for its
own dev/stg/prd environments).
Part 2 of 2
This guide assumes a working cluster from GCP (Terraform), or any GKE cluster with Workload Identity enabled. It deploys the app; it does not provision infrastructure.
Overview
Rather than a bare helm install, this guide bootstraps ArgoCD and lets it manage everything
declaratively: platform add-ons first (cert-manager, External Secrets Operator, ingress-nginx),
then the Rhesis Helm chart itself, ordered by ArgoCD sync-waves.
What the chart deploys:
| Component | Required? | Notes |
|---|---|---|
| Backend | Required | FastAPI application |
| Frontend | Required | Next.js UI |
| Worker | Required | Celery background tasks |
| Valkey (Redis) | Required | Bundled subchart: cache + Celery broker |
| PostgreSQL | Required | Bundled subchart (recommended to start) or your own external instance |
| Chatbot | Optional | Test-the-tests chatbot |
| Polyphemus | Optional | Adversarial test-data model |
| Docs | Optional | Self-hosted copy of this documentation site |
Prerequisite: your own git repository
ArgoCD’s Application resources declare a source.repoURL it pulls from and reconciles
against. Rhesis’s own manifests point at github.com/rhesis-ai/rhesis.git, which you cannot
push to. Fork the repository so you have a repoURL you control. You’ll commit your
cluster overlay and your Helm values file into that fork, in the same layout Rhesis uses
internally (kubernetes/clusters/<env>/ and charts/rhesis/values-<env>.yaml).
Replace <your-org>/rhesis in every manifest below with your fork’s path.
Other prerequisites
| Requirement | Notes |
|---|---|
kubectl | Pointed at your cluster (see the Terraform guide's connect step) |
`argocd` CLI | [Install](https://argo-cd.readthedocs.io/en/stable/cli_installation/) it; used for verification, upgrades, and troubleshooting later in this guide |
| A Kubernetes cluster | From the GCP (Terraform) guide, or any cluster with Workload Identity |
| ESO service account | From the Terraform guide's `external-secrets/gcp` module output |
| A domain (optional) | For public ingress hostnames; skip if you'll smoke-test via port-forward only |
Bootstrap ArgoCD
This is the exact command Rhesis’s own environments run: ArgoCD is bootstrapped by hand once
per cluster (kubectl apply -k kubernetes/bootstrap/argocd/, not a Terraform module), then
manages itself and everything else declaratively from there.
Retrieve the initial admin password, then port-forward and log in with the argocd CLI (also
used for verification and upgrades later in this guide):
Open https://localhost:8080 in a browser to use the UI, or continue with argocd CLI commands
against this same port-forward.
Set up your cluster overlay
Your fork already has the manifests you need under kubernetes/base/: cert-manager,
external-secrets, and both ingress-nginx variants are pre-built there, each with a PLACEHOLDER
wherever a value has to be per-cluster. Rhesis’s own kubernetes/clusters/dev/ wires these into
a running cluster with ArgoCD’s app-of-apps pattern: a small wrapper Application per component
points at a per-cluster kustomize overlay, which composes the matching kubernetes/base/
directory and fills in only the handful of values that differ. Build
kubernetes/clusters/customer/ the same way; you’re reusing the existing base manifests, not
recreating them.
Every wrapper Application has the identical shape; it just points ArgoCD at its own overlay
folder:
external-secrets.yaml and ingress-nginx-external.yaml are the same file with name and
path swapped to match (external-secrets, ingress-nginx-external).
external-secrets overlay
kubernetes/base/external-secrets already defines the namespace, the ClusterSecretStore, and
the ESO Helm chart Application, with PLACEHOLDER for your GCP project ID and ESO service
account email. Compose it and fill those in, the same way
kubernetes/clusters/dev/external-secrets/kustomization.yaml does for dev:
The service account email is the eso service account output by the Terraform guide’s
external-secrets/gcp module: check it with terraform output eso_service_account_email (add
that output if you haven’t already) or gcloud iam service-accounts list --filter="displayName:eso*".
cert-manager overlay
kubernetes/base/cert-manager bundles a working HTTP-01 letsencrypt-prod ClusterIssuer,
alongside two others Rhesis’s own environments use (letsencrypt-staging, and
letsencrypt-prod-dns01 for Cloudflare; see
Match Rhesis’s own setup). Point the HTTP-01 issuer at your
own email and ingress class:
This is an HTTP-01 challenge, which needs no DNS provider API: just your domain’s DNS pointing
at the ingress-nginx LoadBalancer IP (created below) before certificates can issue.
ingress-nginx overlay
Use kubernetes/base/ingress-nginx-external as-is, a plain public LoadBalancer ingress-nginx
install with no per-cluster values to fill in:
Together, cert-manager and ingress-nginx get traffic to the app over HTTPS:
Rhesis’s own environments also run a second, VPN-only internal ingress class for admin
surfaces like ArgoCD and Grafana (optional, covered in
Match Rhesis’s own setup).
The rhesis app itself
kubernetes/clusters/dev/rhesis/rhesis-application.yaml is the Application that deploys the
Helm chart itself. Copy it to kubernetes/clusters/customer/rhesis/rhesis-application.yaml and
change two fields: source.repoURL to your fork, and source.helm.valueFiles to
[values.yaml, values-customer.yaml].
Configure public hostnames
The chart’s default values (charts/rhesis/values.yaml) disable ingress and default to the
internal class: Rhesis’s own components stay VPN-only unless a values file explicitly
publishes them (compare values-dev.yaml, which doesn’t, against values-prd.yaml, which sets
className: external per component). Enable ingress and switch to external for the two
components the public needs to reach:
Minimal vs. full deployment
Only 4 of the 6 chart components have public images. Rhesis publishes
ghcr.io/rhesis-ai/{backend,worker,frontend,chatbot}:latest (manually built, not continuously
updated). docs and polyphemus have no public image: the repo has Dockerfiles for both,
but you must build and push them to your own registry to use them.
Start with those two disabled so your first deploy only needs the public images:
Choose your database/cache strategy
Recommended for a first deployment: the bundled subcharts. Valkey (Redis) is enabled by default; enable the bundled PostgreSQL subchart too:
Rhesis’s own dev environment swaps in a pgvector/pgvector image and custom init scripts for
vector-search support; see charts/rhesis/values-dev.yaml for that pattern if you need it. The
config above uses the chart’s default Bitnami PostgreSQL image, which is simpler to reason about
for a first deployment but does not include the vector extension.
For production scale, Rhesis’s own stg/prd environments instead run
CloudNativePG as a separate ArgoCD-managed operator and point the
chart at it via externalDatabase.host + database.existingSecret; treat that as a follow-up
once your first deployment is working (see Where to go from here).
Populate secrets
The chart only requires one Kubernetes Secret to exist, named rhesis-app-secrets
(existingSecret in values.yaml). Create the minimum set of entries in GCP Secret Manager,
then sync them into the cluster with an ExternalSecret.
| Secret Manager key | Maps to | How to generate |
|---|---|---|
| customer-app-db-user | APP_DB_USER | e.g. `rhesis-user` |
| customer-app-db-pass | APP_DB_PASS | Random string |
| customer-db-encryption-key | DB_ENCRYPTION_KEY | python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" |
| customer-jwt-secret-key | JWT_SECRET_KEY | Random string, 32+ chars |
| customer-nextauth-secret | NEXTAUTH_SECRET | openssl rand -base64 32 |
| customer-session-secret-key | SESSION_SECRET_KEY | Random string, 32+ chars |
customer-redis-password | REDIS_PASSWORD | Random string |
| customer-rhesis-api-key | RHESIS_API_KEY | From https://app.rhesis.ai/tokens (or configure your own AI provider instead) |
ESO watches this ExternalSecret, reads the referenced keys from Secret Manager, and keeps the
resulting rhesis-app-secrets Secret in sync:
This is a minimal set. Optional features documented on the
Environment Variables page (OAuth sign-in, SMTP,
your own AI provider instead of RHESIS_API_KEY, SSO) each add their own keys to this same
ExternalSecret. See kubernetes/clusters/dev/external-secrets/rhesis-app-secrets.yaml in the
repo for the full ~50-key reference Rhesis’s own environments use.
This file is already listed as a resource in the external-secrets/kustomization.yaml overlay
from Set up your cluster overlay; nothing else to wire up.
Apply the root Application
ArgoCD syncs in sync-wave order: External Secrets Operator and cert-manager first, then the
ClusterSecretStore and your ExternalSecret, then ingress-nginx, and finally the rhesis
Helm chart Application.
Verify
Point your domain’s DNS A record at the ingress-nginx LoadBalancer external IP
(kubectl get svc -n ingress-nginx), then, once TLS certificates issue:
Or skip DNS/ingress entirely for a first smoke test:
Optional: match Rhesis’s own setup
Everything above is enough to run Rhesis. Rhesis’s own dev/stg/prd environments add four more pieces on top, each independently adoptable; pick the ones you need rather than all four.
Split ingress into public and internal classes
Add a second wrapper Application, ingress-nginx-internal, reusing
kubernetes/base/ingress-nginx-internal (same shape as cert-manager.yaml, with name and
path swapped). It needs two per-cluster values: the reserved internal IP and the ILB subnet name, both
created by the Terraform guide’s ingress/gcp module, filled in the same way
kubernetes/clusters/dev/ingress-nginx-internal/kustomization.yaml does for dev:
internal is this base manifest’s default IngressClass: anything without an explicit class
lands there. Set className: external explicitly (as in
Configure public hostnames) on anything you want publicly
reachable; leave ArgoCD, Grafana, or a monitoring dashboard unset and they stay VPN-only. See
Add internal DNS below for making those hostnames resolvable.
Automate public DNS
kubernetes/base/external-dns already bundles the namespace, the Cloudflare ExternalSecret,
and the ExternalDNS Application; add it as a wrapper Application the same way as
cert-manager.yaml, with an overlay that fills in your Secret Manager key, domain, and TXT owner
ID, mirroring kubernetes/clusters/dev/external-dns/kustomization.yaml:
The base Application already sets ingressClassFilters: [external], so it only publishes records
for the ingress class from Configure public hostnames; anything
on internal stays unpublished. cloudflare-api-token-customer is the Secret Manager key from
the Terraform guide.
Add internal DNS
For hostnames on the internal ingress class to resolve to anything, you need a DNS server that
answers for them: Rhesis’s BIND9-on-the-WireGuard-VM, covered in the
Terraform guide.
kubernetes/base/internal-dns bundles a second, independent ExternalDNS instance that keeps that
zone in sync; overlay it the same way as
kubernetes/clusters/dev/internal-dns/kustomization.yaml:
The TSIG key comes from the Terraform guide’s internal DNS module.
Put ArgoCD behind the internal ingress
With the internal class and internal DNS in place, expose ArgoCD’s own UI the way Rhesis does:
reachable only over the VPN, instead of kubectl port-forward. Copy
kubernetes/clusters/dev/argocd/argocd-ingress.yaml to
kubernetes/clusters/customer/argocd/argocd-ingress.yaml, changing only the host field, and
add it as one more resource in your top-level kustomization.yaml.
It references letsencrypt-prod-dns01, already bundled in kubernetes/base/cert-manager, using
Cloudflare DNS-01 instead of HTTP-01 (needed since an internal-only host can’t complete an
HTTP-01 challenge). Extend the cert-manager overlay you already wrote to
also fill in that issuer’s email and Cloudflare token secret:
cloudflare-cert-api-token-customer is a separate Cloudflare token (scoped to Zone.DNS: Edit)
from the one ExternalDNS uses above; Rhesis’s own environments give cert-manager and ExternalDNS
their own token/secret pair, even though both call the same Cloudflare API.
Enabling the rest
To bring up docs and polyphemus:
- Build and push each image to your own registry (Dockerfiles:
apps/polyphemus,docs/src). - Set
global.registryinvalues-customer.yamlto that registry, and set each component’s.enabled: true. - Commit and push: ArgoCD’s
selfHeal: truepicks up the change automatically. To force it immediately:argocd app sync rhesis.
Upgrade / Uninstall
Upgrade by editing values-customer.yaml and pushing: ArgoCD reconciles automatically. For
a one-off image tag bump without a commit (matching how Rhesis’s own CI deploys):
Uninstall:
Troubleshooting
ArgoCD app stuck OutOfSync or Degraded: check argocd app get rhesis for the specific
resource; a common cause is the ExternalSecret not having synced yet (see below).
ImagePullBackOff: for the GHCR images, no registry auth is needed (public images). For
images in your own registry, confirm the GKE node service account or a dedicated pull secret has
artifactregistry.reader on the repo.
ExternalSecret stuck, secret never appears: check
kubectl describe externalsecret rhesis-app-secrets -n rhesis; the usual cause is the ESO
service account missing secretmanager.secretAccessor on one of the referenced keys, or a typo
in a remoteRef.key.
Pods stuck Pending: usually an unbound PersistentVolumeClaim; check
kubectl get pvc -n rhesis and confirm your cluster has a default StorageClass
(kubectl get storageclass); GKE provides one by default.
Where to go from here
Beyond this first deployment, Rhesis’s own production environment adds, all optional and independently adoptable:
- Switch to CloudNativePG for managed Postgres backups, replacing the bundled
postgresqlsubchart; seekubernetes/clusters/prd/cnpg-cluster/ - Turn on autoscaling: set
*.hpa.enabled: trueper component; already wired into the chart - Add monitoring:
kube-prometheus-stack, Loki, and Alloy, deployed the same way as ArgoCD Applications inkubernetes/base/ - Match the rest of Rhesis’s own setup: see above for the internal/external ingress split, DNS automation, and internal-only ArgoCD
None of these are required to run Rhesis; they’re the same building blocks Rhesis itself adds on top of the deployment you just completed.