> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognisafe.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Kubernetes Helm Install

> Install Cognisafe on any Kubernetes cluster using the official Helm chart

<Note>
  The Helm chart targets Kubernetes 1.26+ and Helm 3.12+. It installs the proxy, API, safety worker, and web dashboard as separate Deployments, with optional sub-charts for PostgreSQL (via Bitnami) and Redis.
</Note>

## Prerequisites

| Requirement | Minimum version                | Notes                                                              |
| ----------- | ------------------------------ | ------------------------------------------------------------------ |
| Kubernetes  | 1.26                           | Tested on AKS, EKS, GKE, and k3s                                   |
| Helm        | 3.12                           | Earlier versions lack OCI support needed by sub-chart dependencies |
| kubectl     | matching cluster               | Must have `create`, `get`, `list` on the target namespace          |
| PostgreSQL  | 14 (TimescaleDB 2.x extension) | Can use the bundled sub-chart or any managed instance              |
| Redis       | 7.x                            | Used as the safety-scoring job queue                               |

The proxy listens on port 8080, the API on 8000, and the web dashboard on 3000. Your ingress controller must be able to route to all three if you expose them externally.

## Add the Helm Repository

```bash theme={null}
helm repo add cognisafe https://charts.cognisafe.uk
helm repo update
```

Inspect available chart versions:

```bash theme={null}
helm search repo cognisafe --versions
```

## Inspect Default Values

```bash theme={null}
helm show values cognisafe/cognisafe
```

Key sections explained:

| Key                      | Default  | Purpose                                                                         |
| ------------------------ | -------- | ------------------------------------------------------------------------------- |
| `proxy.replicas`         | `2`      | Horizontal replica count for the Go proxy                                       |
| `proxy.image.tag`        | `latest` | Pinned image tag; always pin to a semver in production                          |
| `api.replicas`           | `2`      | FastAPI backend replica count                                                   |
| `worker.replicas`        | `2`      | Safety-worker replica count; scale independently under load                     |
| `web.replicas`           | `1`      | Next.js dashboard; stateless, can scale but rarely necessary                    |
| `postgresql.enabled`     | `true`   | Deploys Bitnami PostgreSQL sub-chart                                            |
| `redis.enabled`          | `true`   | Deploys Bitnami Redis sub-chart                                                 |
| `secrets.existingSecret` | `""`     | Pull credentials from a pre-existing K8s Secret instead of chart-managed values |

## Full values.yaml Example

```yaml theme={null}
# values.yaml — production-grade configuration
global:
  imageRegistry: "ghcr.io"
  imagePullSecrets:
    - name: ghcr-pull-secret   # omit if using public registry

# ---------- Proxy ----------
proxy:
  replicas: 3
  image:
    repository: ghcr.io/cognisafe/proxy
    tag: "1.4.2"
    pullPolicy: IfNotPresent
  resources:
    requests:
      cpu: "250m"
      memory: "128Mi"
    limits:
      cpu: "1000m"
      memory: "512Mi"
  env:
    UPSTREAM_URL: "https://api.openai.com"
    API_BACKEND_URL: "http://cognisafe-api:8000"
  service:
    type: ClusterIP
    port: 8080
  hpa:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilizationPercentage: 70

# ---------- API ----------
api:
  replicas: 2
  image:
    repository: ghcr.io/cognisafe/api
    tag: "1.4.2"
    pullPolicy: IfNotPresent
  resources:
    requests:
      cpu: "500m"
      memory: "512Mi"
    limits:
      cpu: "2000m"
      memory: "2Gi"
  env:
    # Secrets should come from secrets.existingSecret in production
    STRIPE_PRICE_PRO: "price_1Abc123"
    STRIPE_PRICE_TEAM: "price_1Def456"
    SCORER_MODEL: "gpt-4o-mini"
  service:
    type: ClusterIP
    port: 8000

# ---------- Safety Worker ----------
worker:
  replicas: 3
  image:
    repository: ghcr.io/cognisafe/api   # same image, different entrypoint
    tag: "1.4.2"
    pullPolicy: IfNotPresent
  command: ["python", "workers/safety_scorer.py"]
  resources:
    requests:
      cpu: "250m"
      memory: "512Mi"
    limits:
      cpu: "1000m"
      memory: "1Gi"
  hpa:
    enabled: true
    minReplicas: 2
    maxReplicas: 20
    targetCPUUtilizationPercentage: 60

# ---------- Web Dashboard ----------
web:
  replicas: 2
  image:
    repository: ghcr.io/cognisafe/web
    tag: "1.4.2"
    pullPolicy: IfNotPresent
  resources:
    requests:
      cpu: "100m"
      memory: "256Mi"
    limits:
      cpu: "500m"
      memory: "512Mi"
  ingress:
    enabled: true
    className: "nginx"
    host: "cognisafe.example.com"
    tls:
      enabled: true
      secretName: "cognisafe-web-tls"   # pre-created cert-manager Certificate
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt-prod"
      nginx.ingress.kubernetes.io/ssl-redirect: "true"

# ---------- PostgreSQL (sub-chart) ----------
# Set postgresql.enabled: false and supply externalDatabase.url
# if you are using RDS, Cloud SQL, Azure Database, etc.
postgresql:
  enabled: false   # recommended for production — use a managed instance

externalDatabase:
  url: ""   # set via secrets.existingSecret — key: POSTGRES_URL
  # Format: postgresql+asyncpg://user:pass@host:5432/cognisafe

# ---------- Redis (sub-chart) ----------
redis:
  enabled: false   # recommended for production — use ElastiCache / Azure Cache / Upstash

externalRedis:
  url: ""   # set via secrets.existingSecret — key: REDIS_URL
  # Format: redis://:password@host:6379/0

# ---------- Secrets ----------
# When existingSecret is set the chart reads all sensitive env vars
# (POSTGRES_URL, REDIS_URL, STRIPE_SECRET_KEY, PROXY_API_KEY, OPENAI_API_KEY,
#  STRIPE_WEBHOOK_SECRET, INTERNAL_API_SECRET, CLERK_SECRET_KEY) from that Secret.
# The Secret must exist before helm install.
secrets:
  existingSecret: "cognisafe-secrets"

# ---------- Ingress for API and Proxy ----------
apiIngress:
  enabled: true
  className: "nginx"
  host: "api.cognisafe.example.com"
  tls:
    enabled: true
    secretName: "cognisafe-api-tls"

proxyIngress:
  enabled: true
  className: "nginx"
  host: "proxy.cognisafe.example.com"
  tls:
    enabled: true
    secretName: "cognisafe-proxy-tls"

# ---------- ServiceMonitor (kube-prometheus-stack) ----------
serviceMonitor:
  enabled: true
  namespace: monitoring
  interval: "30s"
  path: /metrics
```

## Create the K8s Secret Before Installing

```bash theme={null}
kubectl create namespace cognisafe

kubectl create secret generic cognisafe-secrets \
  --namespace cognisafe \
  --from-literal=POSTGRES_URL="postgresql+asyncpg://user:pass@rds-host:5432/cognisafe" \
  --from-literal=REDIS_URL="redis://:password@cache-host:6379/0" \
  --from-literal=PROXY_API_KEY="csk_live_..." \
  --from-literal=OPENAI_API_KEY="sk-..." \
  --from-literal=STRIPE_SECRET_KEY="sk_live_..." \
  --from-literal=STRIPE_WEBHOOK_SECRET="whsec_..." \
  --from-literal=INTERNAL_API_SECRET="$(openssl rand -hex 32)" \
  --from-literal=CLERK_SECRET_KEY="sk_live_..." \
  --from-literal=NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_live_..."
```

## Install

```bash theme={null}
helm install cognisafe cognisafe/cognisafe \
  --namespace cognisafe \
  --create-namespace \
  --values values.yaml \
  --wait \
  --timeout 10m
```

Quick-start with `--set` overrides (non-production):

```bash theme={null}
helm install cognisafe cognisafe/cognisafe \
  --namespace cognisafe \
  --create-namespace \
  --set postgresql.enabled=true \
  --set redis.enabled=true \
  --set proxy.replicas=1 \
  --set api.replicas=1 \
  --set worker.replicas=1 \
  --set web.ingress.enabled=false
```

## Helm Hooks: Database Migration

The chart ships a pre-install / pre-upgrade Job that runs `alembic upgrade head` before any Deployment rollout. It uses the same API image and the same `POSTGRES_URL` secret.

```yaml theme={null}
# Excerpt from the chart — hooks/migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: cognisafe-migrate
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: ghcr.io/cognisafe/api:{{ .Values.api.image.tag }}
          command: ["alembic", "upgrade", "head"]
          envFrom:
            - secretRef:
                name: {{ .Values.secrets.existingSecret }}
```

If the migration job fails, Helm will abort the install/upgrade and roll back automatically.

## Upgrade

```bash theme={null}
# Pull latest chart metadata
helm repo update

# Upgrade in place — migration job runs first
helm upgrade cognisafe cognisafe/cognisafe \
  --namespace cognisafe \
  --values values.yaml \
  --wait \
  --timeout 10m
```

To upgrade only the image tag without changing values:

```bash theme={null}
helm upgrade cognisafe cognisafe/cognisafe \
  --namespace cognisafe \
  --reuse-values \
  --set proxy.image.tag=1.5.0 \
  --set api.image.tag=1.5.0 \
  --set worker.image.tag=1.5.0 \
  --set web.image.tag=1.5.0
```

## Uninstall and PVC Cleanup

```bash theme={null}
helm uninstall cognisafe --namespace cognisafe
```

Helm does not delete PersistentVolumeClaims by default (to protect data). Delete them explicitly after confirming the database is no longer needed:

```bash theme={null}
kubectl delete pvc -n cognisafe --all
```

<Warning>
  Deleting PVCs is irreversible. Take a final database snapshot before running this command in production.
</Warning>

## Multi-Namespace / Multi-Tenant Pattern

Each tenant gets their own Helm release in an isolated namespace. A shared PostgreSQL cluster is recommended; use a separate database per tenant rather than separate clusters.

```bash theme={null}
# Tenant A
kubectl create namespace tenant-a
kubectl create secret generic cognisafe-secrets \
  --namespace tenant-a \
  --from-literal=POSTGRES_URL="postgresql+asyncpg://user:pass@shared-pg:5432/tenant_a" \
  # ... other secrets

helm install cognisafe-tenant-a cognisafe/cognisafe \
  --namespace tenant-a \
  --values values-tenant-a.yaml

# Tenant B — identical pattern, different namespace and DB
kubectl create namespace tenant-b
helm install cognisafe-tenant-b cognisafe/cognisafe \
  --namespace tenant-b \
  --values values-tenant-b.yaml
```

Use Kubernetes NetworkPolicy to prevent cross-namespace traffic:

```yaml theme={null}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-cross-tenant
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: tenant-a
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: tenant-a
    - ports:  # allow DNS
        - port: 53
          protocol: UDP
```

## Air-Gapped / Private Registry Deployment

Mirror the three images to your internal registry, then override the image references:

```bash theme={null}
# Mirror images
for img in proxy api web; do
  docker pull ghcr.io/cognisafe/${img}:1.4.2
  docker tag ghcr.io/cognisafe/${img}:1.4.2 registry.internal.corp/cognisafe/${img}:1.4.2
  docker push registry.internal.corp/cognisafe/${img}:1.4.2
done
```

```yaml theme={null}
# values-airgap.yaml
global:
  imageRegistry: "registry.internal.corp"
  imagePullSecrets:
    - name: internal-registry-pull-secret

proxy:
  image:
    repository: registry.internal.corp/cognisafe/proxy
    tag: "1.4.2"

api:
  image:
    repository: registry.internal.corp/cognisafe/api
    tag: "1.4.2"

web:
  image:
    repository: registry.internal.corp/cognisafe/web
    tag: "1.4.2"
```

```bash theme={null}
helm install cognisafe cognisafe/cognisafe \
  --namespace cognisafe \
  --values values.yaml \
  --values values-airgap.yaml
```

<Tip>
  For fully air-gapped clusters, package the chart as a tarball and carry it in: `helm package cognisafe/cognisafe && helm install cognisafe ./cognisafe-1.4.2.tgz --namespace cognisafe --values values.yaml`
</Tip>
