Skip to content

Kubernetes Deployment Guide

This guide covers deploying DocuElevate on Kubernetes using the provided Helm chart.

Quick reference: For a side-by-side comparison of Docker Compose vs. Kubernetes deployment options, see the Deployment Guide.

Table of Contents


Prerequisites

Requirement Minimum Version Notes
Kubernetes 1.24 1.27+ recommended
Helm 3.10
Storage Class (RWX) Required for multi-replica; NFS, CephFS, Azure Files, EFS, etc.
PostgreSQL 14+ Strongly recommended; SQLite not safe for multi-replica
cert-manager 1.12+ Optional, for automated TLS via Let's Encrypt

The Helm chart is located in the repository at helm/docuelevate/.


Architecture Overview

Internet
   │
   ▼
[Ingress Controller]         ← TLS termination, host routing
   │
   ▼
[API Deployment]             ← FastAPI web server (multiple replicas)
   │      │
   │      └── [Shared PVC: /workdir]  ← ReadWriteMany volume
   │                │
   ▼                ▼
[Worker Deployment]          ← Celery background task workers (multiple replicas)
   │
   ├── [Redis Service]       ← Celery broker & result backend
   ├── [Gotenberg Service]   ← Document → PDF conversion
   └── [Meilisearch Service] ← Full-text search index

All services communicate over the cluster's internal network. Redis and Meilisearch must not be exposed outside the cluster.


Quick Start

1. Add Chart Dependencies

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependency update ./helm/docuelevate

2. Create a Values Override File

Create my-values.yaml (never commit this file — it contains secrets):

env:
  EXTERNAL_HOSTNAME: docuelevate.example.com
  AZURE_ENDPOINT: "https://my-resource.cognitiveservices.azure.com/"
  AUTH_ENABLED: "true"

secrets:
  DATABASE_URL: "postgresql://docuelevate:strongpassword@postgres:5432/docuelevate"
  SESSION_SECRET: "<run: openssl rand -hex 32>"
  OPENAI_API_KEY: "sk-..."
  AZURE_AI_KEY: "..."

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: docuelevate.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: docuelevate-tls
      hosts:
        - docuelevate.example.com

3. Install the Chart

helm install docuelevate ./helm/docuelevate \
  --namespace docuelevate \
  --create-namespace \
  -f my-values.yaml

4. Verify the Deployment

kubectl get pods -n docuelevate
kubectl get svc -n docuelevate
kubectl get ingress -n docuelevate

Wait until all pods report Running and 1/1 (or 2/2 for multi-container pods).


Helm Values Reference

The complete list of values is in helm/docuelevate/values.yaml. Key sections are summarized below.

Container Image

image:
  repository: ghcr.io/christianlouis/docuelevate
  tag: ""            # Defaults to chart appVersion; pin a specific tag in production
  pullPolicy: IfNotPresent

Non-Secret Configuration (env)

env:
  WORKDIR: /workdir
  AI_PROVIDER: openai
  OPENAI_MODEL: gpt-4o-mini
  AZURE_REGION: eastus
  AZURE_ENDPOINT: "https://my-resource.cognitiveservices.azure.com/"
  MEILISEARCH_URL: http://docuelevate-meilisearch:7700
  ENABLE_SEARCH: "true"
  AUTH_ENABLED: "true"
  EXTERNAL_HOSTNAME: docuelevate.example.com
  ALLOW_FILE_DELETE: "true"

Secrets (secrets)

Secrets are stored in a Kubernetes Secret resource and injected as environment variables.

secrets:
  DATABASE_URL: "postgresql://user:pass@postgres:5432/docuelevate"
  SESSION_SECRET: "<min-32-char-random-string>"
  OPENAI_API_KEY: "sk-..."
  AZURE_AI_KEY: "..."
  MEILISEARCH_API_KEY: ""      # Leave blank for unauthenticated local Meilisearch
  DROPBOX_APP_KEY: ""          # Optional — only if using Dropbox
  DROPBOX_APP_SECRET: ""
  GOOGLE_DRIVE_CLIENT_ID: ""   # Optional — only if using Google Drive
  GOOGLE_DRIVE_CLIENT_SECRET: ""

Tip: In production, manage secrets with an external secret manager. See Secrets Management.


Storage Configuration

Shared Workdir PVC

Both the API and Worker pods need to access the same /workdir volume for document staging.

workdir:
  persistence:
    enabled: true
    accessMode: ReadWriteMany   # Required when api.replicaCount > 1 or worker.replicaCount > 1
    size: 50Gi
    storageClass: "nfs-client"  # Must support ReadWriteMany

Single-replica clusters can use ReadWriteOnce:

workdir:
  persistence:
    accessMode: ReadWriteOnce
    size: 20Gi
    storageClass: ""   # Use cluster default

Meilisearch Data

Meilisearch data is stored in a separate PVC:

meilisearch:
  enabled: true
  persistence:
    enabled: true
    size: 10Gi
    storageClass: ""   # Use cluster default (RWO is fine here)

Database Setup

For production, deploy PostgreSQL externally (managed service or a separate Helm release) and set DATABASE_URL in secrets.

External PostgreSQL

secrets:
  DATABASE_URL: "postgresql://docuelevate:password@my-postgres-host:5432/docuelevate?sslmode=require"

If you must use a bundled PostgreSQL instance, add it as a Helm dependency or deploy the Bitnami PostgreSQL chart in the same namespace. The Helm chart does not bundle PostgreSQL by default.

Database Migrations

A Kubernetes Job is included in the Helm chart as a pre-install and pre-upgrade hook. It runs alembic upgrade head before any pods are updated:

# This is automatic — no additional configuration needed

To run migrations manually:

kubectl run alembic-upgrade \
  --image=ghcr.io/christianlouis/docuelevate:latest \
  --namespace=docuelevate \
  --restart=Never \
  --env-from=secret/docuelevate-secrets \
  -- alembic upgrade head

See the Database Configuration Guide for detailed database setup.


Secrets Management

Option 1: Values File (Basic)

Store secrets in my-values.yaml and never commit it to source control. Pass it with -f my-values.yaml at install/upgrade time.

Use External Secrets Operator with HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault:

# ExternalSecret resource
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: docuelevate-secrets
  namespace: docuelevate
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: docuelevate-secrets
    creationPolicy: Owner
  data:
    - secretKey: DATABASE_URL
      remoteRef:
        key: docuelevate/production
        property: database_url
    - secretKey: SESSION_SECRET
      remoteRef:
        key: docuelevate/production
        property: session_secret
    - secretKey: OPENAI_API_KEY
      remoteRef:
        key: docuelevate/production
        property: openai_api_key

Then reference the pre-existing secret in Helm values:

existingSecret: docuelevate-secrets   # Use this key if the chart supports it

Option 3: Sealed Secrets

Use Bitnami Sealed Secrets to encrypt secrets before committing to Git.


Ingress & TLS

Nginx Ingress

ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "1g"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: docuelevate.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: docuelevate-tls
      hosts:
        - docuelevate.example.com

Traefik Ingress

ingress:
  enabled: true
  className: traefik
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: docuelevate.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: docuelevate-tls
      hosts:
        - docuelevate.example.com

Manual TLS Secret

If you manage TLS certificates outside cert-manager:

kubectl create secret tls docuelevate-tls \
  --cert=path/to/fullchain.pem \
  --key=path/to/privkey.pem \
  --namespace=docuelevate

Scaling & Autoscaling

Manual Scaling

api:
  replicaCount: 3

worker:
  replicaCount: 4

Beat scheduler: The Helm chart deploys a dedicated beat pod (always exactly 1 replica with Recreate strategy) that publishes periodic tasks to the Redis broker. Workers consume these tasks — scaling workers does not duplicate scheduled jobs.

Horizontal Pod Autoscaler

api:
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 8
    targetCPUUtilizationPercentage: 70

worker:
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilizationPercentage: 75

Prerequisite: The Kubernetes Metrics Server must be installed in your cluster for HPA to function.

Resource Requests and Limits

api:
  resources:
    requests:
      cpu: "250m"
      memory: "512Mi"
    limits:
      cpu: "1000m"
      memory: "2Gi"

worker:
  resources:
    requests:
      cpu: "500m"
      memory: "1Gi"
    limits:
      cpu: "2000m"
      memory: "4Gi"

External Redis

Disable the bundled Redis and point at an external instance for greater resilience:

redis:
  enabled: false

externalRedis:
  url: "redis://my-redis-cluster:6379/0"

Monitoring & Health Checks

Kubernetes Probes

The Helm chart configures unauthenticated liveness and readiness probes on the API pods so kubelet can reach them without credentials. Default settings:

api:
  livenessProbe:
    httpGet:
      path: /api/diagnostic/healthz/live
      port: 8000
    initialDelaySeconds: 30
    periodSeconds: 20
  readinessProbe:
    httpGet:
      path: /api/diagnostic/healthz/ready
      port: 8000
    initialDelaySeconds: 15
    periodSeconds: 10
Endpoint Auth Purpose
/api/diagnostic/healthz/live None Lightweight liveness check — returns 200 if the process is running
/api/diagnostic/healthz/ready None Readiness check — verifies database and Redis connectivity (503 when DB is down)
/api/diagnostic/health Required Full health status for monitoring dashboards (Grafana, Uptime Kuma)

Prometheus Scraping

Add annotations to expose metrics (if using a Prometheus-compatible exporter):

api:
  podAnnotations:
    prometheus.io/scrape: "true"
    prometheus.io/path: "/metrics"
    prometheus.io/port: "8000"

Pod Disruption Budget

Ensure availability during node maintenance:

kubectl apply -f - <<EOF
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: docuelevate-api-pdb
  namespace: docuelevate
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/component: api
      app.kubernetes.io/instance: docuelevate
EOF

Upgrades

helm upgrade docuelevate ./helm/docuelevate \
  --namespace docuelevate \
  -f my-values.yaml

The pre-upgrade hook automatically runs alembic upgrade head before new pods are created. Upgrades are rolling by default — old pods continue to serve traffic until new pods are ready.

Image tag pinning (recommended):

image:
  tag: "1.5.2"   # Pin a specific version tag instead of using 'latest'

Uninstalling

helm uninstall docuelevate --namespace docuelevate

Warning: Persistent Volume Claims are NOT deleted automatically. Remove them manually if you no longer need the data:

kubectl delete pvc -l app.kubernetes.io/instance=docuelevate -n docuelevate

To delete the namespace entirely:

kubectl delete namespace docuelevate

Troubleshooting

Pods Stuck in Pending

kubectl describe pod <pod-name> -n docuelevate

Common causes: - No available nodes with sufficient CPU/memory — adjust resource requests or add nodes. - PVC cannot be bound — verify the StorageClass supports the required accessMode. - Image pull failure — check imagePullSecrets and network access to the container registry.

Pods in CrashLoopBackOff

kubectl logs <pod-name> -n docuelevate --previous

Common causes: - DATABASE_URL is wrong or the database is unreachable. - SESSION_SECRET is missing or too short. - Required environment variable not set in secrets or env.

Migration Job Fails

kubectl logs job/docuelevate-migrate -n docuelevate

Resolve the database connection issue then re-run the job or run the upgrade again.

Workdir Volume Mount Errors

Ensure the StorageClass supports ReadWriteMany when api.replicaCount > 1 or worker.replicaCount > 1. Check your storage provisioner documentation.

For more help, see the Troubleshooting Guide.