Deploy · Helm chart

Kubernetes

The signed Helm chart: one pod on SQLite, or several on PostgreSQL and Redis behind a disruption budget. Installed and upgraded under load on a real cluster in CI, and it refuses setups that can't work.

Stable since 0.30

Values may be added; none is renamed or removed except through the deprecation process. Chart X.Y.Z runs QueryAPIGate X.Y.Z. It installs no PostgreSQL or Redis of its own, and its NetworkPolicy stays off until you give it your cluster's rules. Requires Kubernetes 1.25 or later, with Helm 3 or 4.

Ingress/events and MCP routedDeploymentrolling update · PDBEvents · MCPown podsSecret or VaultexistingSecretServiceMonitortoken-protected /metricsPostgreSQL · RedisYour databases
01…
Helm chart

Kubernetes

✓One pod on SQLite, or several on PostgreSQL and Redis behind a disruption budget.
✓Non-root, read-only root filesystem, no capabilities; a NetworkPolicy and autoscaler if you want them.
✓The chart refuses setups that can't work, such as several pods without Redis.
StoreSQLite (one pod) or PostgreSQL
RedisRequired for more than one pod
Live eventsEvents pods, routed by the ingress
AvailabilityRolling updates, disruption budget
Configurationconfig values, configAsCode
Tested in CIkind cluster: install, upgrade under load
Install

One pod on SQLite

One pod, with queryapigate.db on a 1 Gi PersistentVolumeClaim. The claim is kept on helm uninstall. Reinstall under the same name and namespace and it picks the store up again, keys and all. Upgrades on SQLite replace the pod instead of rolling it, so expect a few seconds of downtime.

shell
kubectl create namespace queryapigate
kubectl -n queryapigate create secret generic queryapigate \
  --from-literal=QUERYAPIGATE_API_KEY=<a long random key> \
  --from-literal=QUERYAPIGATE_SECRET_KEY=<a Fernet key>
helm install queryapigate oci://ghcr.io/anantharajuc/charts/queryapigate --version 0.47.0 \
  -n queryapigate --set existingSecret=queryapigate --wait
Several pods

On PostgreSQL and Redis

The pods roll one at a time, and a new pod must be ready before an old one stops (maxUnavailable: 0). The PodDisruptionBudget keeps all but one up during node drains. Use managed PostgreSQL and Redis in production; deploy/kubernetes/dev-dependencies.yaml starts throwaway ones for a trial.

values-prod.yaml
store: {type: postgres}
replicas: 3
existingSecret: queryapigate      # + QUERYAPIGATE_DATABASE_URL and QUERYAPIGATE_REDIS_URL
redis: {inExistingSecret: true}   # the chart can't see inside your Secret
events: {enabled: true}           # live events for many clients
mcp: {enabled: true}              # the MCP server for AI agents
config:
  QUERYAPIGATE_JSON_LOGS: "1"
What it refuses

Refused before anything changes

helm install and helm upgrade fail without touching the cluster.

  • more than one pod on SQLite,
  • more than one pod without Redis (otherwise rate limits, quotas and max_concurrent would be counted per pod),
  • store.type: postgres without a database URL,
  • a secret setting (a key, or the database or Redis URL) in config, where it would sit in plain text in the Deployment,
  • a value not in values.schema.json. A misspelt replica=3 fails with additional properties 'replica' not allowed.
helm upgrade
Error: UPGRADE FAILED: store.type sqlite keeps the store in one file on one volume - it runs one pod.
For several, set store.type: postgres (QUERYAPIGATE_DATABASE_URL in the secret) and Redis.
Hardening

Locked down by default

Non-root (UID 1000), read-only root filesystem, every capability dropped, RuntimeDefault seccomp, no service account token (QueryAPIGate never calls the Kubernetes API). It writes only to /data and /tmp.

kubectl
$ kubectl exec deploy/queryapigate -- sh -c 'id; touch /x'
uid=1000(app) gid=1000(app) groups=1000(app)
touch: cannot touch '/x': Read-only file system
Metrics need a token/metrics needs a token by default (metricsAuth): the chart creates one in its own Secret, keeps it across upgrades, and the ServiceMonitor sends it.
Values

The values that matter

ConcernValues
SecretsexistingSecret (recommended: sealed-secrets, External Secrets, Vault), or secret.create + secret.values
Other settingsconfig: any QUERYAPIGATE_* or OTEL_*
Routingingress.* sends /events to the events pods and everything else to the API pods, and sets QUERYAPIGATE_TRUST_PROXY=1. mcp.ingress.host gives MCP its own host. The defaults are ingress-nginx no-buffering annotations; replace them for another controller.
Imageimage.digest: pull by the digest you verified
ObservabilityserviceMonitor.enabled, metricsAuth, tracing through OTEL_EXPORTER_OTLP_ENDPOINT
NetworknetworkPolicy.enabled with ingressFrom and egress. List your databases, Redis and SIEM there, or queries can't reach them.
Scaleautoscaling.enabled (CPU-based, PostgreSQL shape only; 2-6 replicas by default)
Secrets managersvaultToken.enabled (a projected, short-lived token for Vault only); serviceAccount.annotations for IRSA
Files by pathextraVolumes / extraVolumeMounts for SIEM certificates or export directories, since the root filesystem is read-only
Upgrades

Upgrades under load

The same helm upgrade with the new --version. In the guide's run, a pod called /q/tables every 50 ms throughout the upgrade: 1,690 answered, 0 failed. Upgrade one minor version at a time, and read its changelog entry first.

In CI on every pushThe chart is linted and rendered with Helm 3.22 and 4.3 (tests/test_helm.py: refusals, hardening, routes, and the plain manifests kept in step). Then it is installed on a kind cluster in the PostgreSQL shape with three pods, events and MCP, and upgraded from the previous release's chart to the current code while a pod keeps calling it. A single failed request fails the build. The same run checks that /metrics answers 401 without the chart's token.
GitOps

Configuration as code

configAsCode.enabled with configAsCode.file (or existingConfigMap) puts connections, queries, roles and keys in one file in git, with no secrets: passwords are ${VAR} references and keys are stored as secret_sha256. The first new API pod applies it as it starts, once per file. A change to the file rolls the pods. A file that can't be applied stops the rollout at its first pod, while the old pods keep serving. locked: true restricts what the file manages to changes through the file, so a Console edit gets 409 config_managed. Argo CD or Flux syncing the chart is then the deploy (guide 55).

Verify

Verify before you run it

The same works for the -h2 image and the chart. Then set image.digest to the verified digest (guide 59).

shell
cosign verify ghcr.io/anantharajuc/queryapigate:0.47.0 \
  --certificate-identity https://github.com/AnanthaRajuC/QueryAPIGate/.github/workflows/docker.yml@refs/tags/v0.47.0 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
Good to know

Good to know

  • Changing a Secret you manage doesn't restart the pods, because the chart can't see inside it. Run kubectl rollout restart on each Deployment (-events and -mcp too). Changes to config, or to a Secret the chart makes, roll the pods on helm upgrade.
  • Backups: on SQLite, kubectl exec deploy/queryapigate -- queryapigate backup /tmp/backup.db --force, then kubectl cp the file out. To restore, scale to 0, put the file on the claim from a one-off pod, remove -wal and -shm, and scale back to 1. On PostgreSQL, use your database's own backups.
  • Without Helm: deploy/kubernetes/queryapigate.yaml is the same deployment as plain manifests, kept in step by a test.

What changes when you move

Moving between shapes is a configuration change. This is what each step adds; the step for this page is highlighted.

01Single instance
QUERYAPIGATE_API_KEY=… QUERYAPIGATE_SECRET_KEY=… One process or container, with the store as SQLite on a volume. The key is required in a container; the secret key encrypts connection passwords at rest.
02+ High availability
QUERYAPIGATE_DATABASE_URL=postgres://… QUERYAPIGATE_REDIS_URL=redis://… A shared PostgreSQL store and Redis, so any instance answers any request and limits are counted once. Run queryapigate events behind the proxy for live events. Guide 42 →
03+ Kubernetes
store.type: postgres · replicas: 3 existingSecret: queryapigate The same two URLs and the key live in one Secret; the chart refuses several pods on SQLite, several without Redis, PostgreSQL without a database URL, and secrets put in config. Guide 54 →
Before go-live The production checklist, the ports and the measured numbers They apply to every shape, so they live on the Deploy overview.