Kubernetes (Helm)
Deploy Vouch on Kubernetes using the Helm chart.
Prerequisites
- Kubernetes cluster (1.24+)
- Helm 3
- A persistent volume provisioner (for SQLite) or external PostgreSQL
Install
# Install from OCI registry
helm install vouch-server oci://ghcr.io/vouch-sh/charts/vouch-server \
--version 0.1.0 \
--namespace vouch \
--create-namespace \
--values my-values.yaml
Values
Key values to configure:
# values.yaml
image:
repository: ghcr.io/vouch-sh/vouch
pullPolicy: IfNotPresent
tag: "" # defaults to chart appVersion
serviceAccount:
create: true
annotations: {}
podSecurityContext:
fsGroup: 65532
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
service:
type: ClusterIP
port: 3000
# Environment variables for vouch-server
env:
VOUCH_LISTEN_ADDR: "0.0.0.0:3000"
VOUCH_DATABASE_URL: "sqlite:/data/vouch.db?mode=rwc"
VOUCH_RP_ID: "auth.example.com"
VOUCH_BASE_URL: "https://auth.example.com"
RUST_LOG: "info,vouch_server=debug"
# Secret environment variables
# Reference an existing secret containing keys like:
# - VOUCH_JWT_SECRET
# - VOUCH_IDPS (e.g., "google")
# - VOUCH_IDP_GOOGLE_TYPE (oidc|saml)
# - VOUCH_IDP_GOOGLE_ISSUER
# - VOUCH_IDP_GOOGLE_CLIENT_ID
# - VOUCH_IDP_GOOGLE_CLIENT_SECRET
existingSecret: ""
# Or create a new secret (puts secret values in this file; use existingSecret in production)
secrets: {}
# VOUCH_JWT_SECRET: ""
# Ingress
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: auth.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: vouch-tls
hosts:
- auth.example.com
# Resources
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# Persistence (for SQLite)
persistence:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
mountPath: /data
# Health check configuration
# Liveness uses /health (static, always 200 while the process is up).
# Readiness uses /health/ready (checks the database, 503 when it is unreachable).
healthcheck:
path: /health
readinessPath: /health/ready
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
Using Kubernetes Secrets
Create secrets for sensitive values:
kubectl create secret generic vouch-secrets \
--namespace vouch \
--from-literal=VOUCH_JWT_SECRET='<your-64-character-secret>' \
--from-literal=VOUCH_IDPS='google' \
--from-literal=VOUCH_IDP_GOOGLE_TYPE='oidc' \
--from-literal=VOUCH_IDP_GOOGLE_ISSUER='https://accounts.google.com' \
--from-literal=VOUCH_IDP_GOOGLE_CLIENT_ID='...' \
--from-literal=VOUCH_IDP_GOOGLE_CLIENT_SECRET='...'
Then reference in values:
existingSecret: vouch-secrets
Air-Gapped Kubernetes
For air-gapped environments:
-
Save and transfer the chart:
helm pull oci://ghcr.io/vouch-sh/charts/vouch-server --version 0.1.0 # Transfer vouch-server-0.1.0.tgz to air-gapped environment -
Save and transfer the container image:
docker pull ghcr.io/vouch-sh/vouch:0.1.0 docker save ghcr.io/vouch-sh/vouch:0.1.0 -o vouch-0.1.0.tar # Transfer and load into your private registry -
Install from the local chart:
helm install vouch-server ./vouch-server-0.1.0.tgz \ --namespace vouch \ --create-namespace \ --set image.repository=registry.internal/vouch \ --values my-values.yaml
Upgrading
helm upgrade vouch-server oci://ghcr.io/vouch-sh/charts/vouch-server \
--version <new-version> \
--namespace vouch \
--values my-values.yaml
Health Checks
Vouch exposes two separate endpoints, and they are not interchangeable:
| Probe | Endpoint | Behavior |
|---|---|---|
| Liveness | /health | Returns 200 with the body ok whenever the process is running. It performs no dependency checks, so it only ever fails if the process is hung or dead — which is exactly what a liveness probe should test. |
| Readiness | /health/ready | Runs SELECT 1 against the database. Returns 200 {"status":"ready"}, or 503 {"status":"not_ready","reason":"database"} when the database is unreachable. |
Important: point the readiness probe at
/health/ready, not/health. A pod whose database connection has failed will keep passing a/healthreadiness probe and stay in the Service’s endpoint list, sending every request to an instance that cannot serve it.
See Monitoring and Metrics for the full endpoint list and the Prometheus metrics.