Kubernetes

A Helm chart addressed by URL, or manifests meant to be read — plus the ingress setting that closes every websocket.

Two shapes, the same deployment. The chart if you want it parameterised, the manifests if you would rather read a deployment than configure one.

Helm

helm install datagripe oci://ghcr.io/datagripe/charts/datagripe \
  --namespace datagripe --create-namespace \
  --set webOrigin=https://datagripe.example.com \
  --set ingress.enabled=true \
  --set ingress.hosts[0].host=datagripe.example.com

The chart is an OCI artifact published beside the image, so there is no helm repo add and no index.yaml to go stale. Its version is DataGripe's version: --version 0.0.16 is the chart that installs 0.0.16.

Upgrades and restarts

The chart's bundled PostgreSQL StatefulSet is an external app database from DataGripe's perspective. Its schema needs the same migration step as a separately managed PostgreSQL service.

The app checks and applies missing migrations on every startup, before serving requests. This also runs when Kubernetes restarts only the app container in an existing pod. Concurrent starts share a database advisory lock, and a failed migration prevents the app from becoming ready.

helm upgrade may pre-apply migrations through its hook, and the plain manifests retain a migration init container. Both are compatible with the startup check: already-recorded files are skipped. Pin the migration and server images to the same release.

For an older server, such as 0.0.13, that has already started without its migrations, apply them from the running image and reload the browser:

kubectl exec -n datagripe deployment/datagripe -- \
  bun run /app/bin/datagripe.mjs migrate

A managed database

Better operated than one you now own:

--set postgresql.enabled=false \
--set database.url=postgres://user:password@db.example.com:5432/datagripe

No database at all

DataGripe runs the PostgreSQL it needs inside the pod. One workload, one volume, strictly one replica:

--set database.mode=embedded \
--set postgresql.enabled=false \
--set persistence.enabled=true

Secrets

The encryption key and the session secret are generated on first install and kept across upgrades. Their Secret is marked helm.sh/resource-policy: keep, so an accidental helm uninstall does not take them with it.

Back them up anyway, or set them yourself:

--set secrets.existingSecret=datagripe-secrets

Losing CONNECTION_ENCRYPTION_KEY does not sign anyone out. It orphans every datasource password in the database, permanently.

Plain manifests

deploy/k8s is the same deployment written to be read top to bottom rather than parameterised: a Deployment with the migration as an init container, a PostgreSQL StatefulSet, a Service, and an Ingress commented out until you have a hostname.

$EDITOR deploy/k8s/secret.yaml      # the secrets
$EDITOR deploy/k8s/configmap.yaml   # WEB_ORIGIN
kubectl apply -k deploy/k8s

One replica

The default is one replica. Migration runners serialize through a database advisory lock, so concurrent starts do not apply a file twice. Embedded database mode and a shared local data volume still require one writer.

Nothing else in DataGripe requires a single replica. Sessions live in the database, not in the pod.

Ingress and websockets

This is the setting that breaks a Kubernetes deployment in a way nothing else explains.

DataGripe holds one WebSocket open per browser tab, for as long as the tab is open. ingress-nginx's default 60-second read timeout closes every one of them a minute after it opens. The app reconnects, so it looks like an intermittent glitch rather than a configuration error — but a query in flight does not survive it.

The chart sets these already. If you are writing your own ingress, set them too:

nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-body-size: 32m

Other ingress controllers have their own equivalent. The requirement is the same: a long-lived upgraded connection, and a body size that fits an import.

WEB_ORIGIN

The exact origin browsers use — which behind an ingress is the ingress hostname and scheme, not the Service and not the pod. Both the HTTP routes and the WebSocket upgrade compare against it, so a mismatch is an app that loads and then does nothing at all.

Set NODE_ENV=production alongside it once that origin is https://.

  • Upgrading — the migration hook, and what to do when it fails.
  • Security — what to switch off for a shared deployment.
  • Configuration — every variable, a page per decision.

This page as Markdown, for an agent or a diff.