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
- Architecture Overview
- Quick Start
- Helm Values Reference
- Storage Configuration
- Database Setup
- Secrets Management
- Ingress & TLS
- Scaling & Autoscaling
- Monitoring & Health Checks
- Upgrades
- Uninstalling
- Troubleshooting
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"
Bundled PostgreSQL (Not Recommended for Production)
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.
Option 2: External Secrets Operator (Recommended)
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
beatpod (always exactly 1 replica withRecreatestrategy) 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.