Kubernetes Deployment v4.2.2¶
Deploy Kreuzberg to Kubernetes with proper OCR configuration, permissions, and health checks.
Helm Chart v4.8.4¶
The recommended way to deploy Kreuzberg on Kubernetes is via the official Helm chart, published as an OCI artifact to GitHub Container Registry.
Install¶
Configure¶
Override defaults with a values.yaml file:
replicaCount: 3
image:
tag: "4.8.4"
kreuzberg:
logLevel: "info"
ocrLanguage: "eng"
resources:
requests:
memory: "1Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
cache:
enabled: true
size: 5Gi
ingress:
enabled: true
className: "nginx"
hosts:
- host: kreuzberg.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: kreuzberg-tls
hosts:
- kreuzberg.example.com
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
podDisruptionBudget:
enabled: true
minAvailable: 1
helm install kreuzberg oci://ghcr.io/kreuzberg-dev/charts/kreuzberg \
--version 4.8.4 \
-f values.yaml
Upgrade¶
What's Included¶
The chart creates the following resources:
| Resource | Description | Conditional |
|---|---|---|
| Deployment | Main application with health probes and security hardening | Always |
| Service | ClusterIP service on port 80 → 8000 | Always |
| ServiceAccount | Dedicated service account | Always |
| PersistentVolumeClaim | Cache for embedding models and assets | cache.enabled |
| Ingress | HTTP(S) ingress with TLS | ingress.enabled |
| HorizontalPodAutoscaler | CPU/memory-based autoscaling | autoscaling.enabled |
| PodDisruptionBudget | Availability during disruptions | podDisruptionBudget.enabled |
All values are documented in the chart's values.yaml.
If you need finer control over the manifests, see the raw YAML examples below.
Quick Start¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: kreuzberg-api
spec:
replicas: 2
selector:
matchLabels:
app: kreuzberg
template:
metadata:
labels:
app: kreuzberg
spec:
containers:
- name: kreuzberg
image: ghcr.io/kreuzberg-dev/kreuzberg:latest
ports:
- containerPort: 8000
name: http
env:
- name: RUST_LOG
value: "info"
- name: TESSDATA_PREFIX
value: "/usr/share/tesseract-ocr/5/tessdata"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: kreuzberg-api
spec:
selector:
app: kreuzberg
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer
Tesseract Configuration¶
TESSDATA_PREFIX (Critical)¶
Without TESSDATA_PREFIX, OCR silently falls back to non-OCR extraction. Official images ship Tesseract 5.x with tessdata at /usr/share/tesseract-ocr/5/tessdata/.
env:
- name: TESSDATA_PREFIX
value: "/usr/share/tesseract-ocr/5/tessdata"
- name: KREUZBERG_OCR_LANGUAGE
value: "eng"
- name: KREUZBERG_CACHE_DIR
value: "/app/.kreuzberg"
- name: HF_HOME
value: "/app/.kreuzberg/huggingface"
Pre-installed languages: eng, spa, fra, deu, ita, por, chi_sim, chi_tra, jpn, ara, rus, hin
Tesseract Version
The path varies by version. Verify yours with tesseract --version inside the container if using a custom base image.
Custom Languages via ConfigMap¶
kubectl create configmap tessdata \
--from-file=/path/to/eng.traineddata \
--from-file=/path/to/deu.traineddata
spec:
containers:
- name: kreuzberg
env:
- name: TESSDATA_PREFIX
value: "/etc/tessdata"
volumeMounts:
- name: tessdata
mountPath: /etc/tessdata
volumes:
- name: tessdata
configMap:
name: tessdata
For large custom language sets, use a PVC instead of a ConfigMap.
Verify Tesseract¶
kubectl exec -it deployment/kreuzberg-api -- tesseract --version
kubectl exec -it deployment/kreuzberg-api -- tesseract --list-langs
kubectl exec -it deployment/kreuzberg-api -- printenv TESSDATA_PREFIX
Permissions¶
Kreuzberg runs as non-root (UID 1000, GID 1000). Fix PVC permissions with either approach:
spec:
initContainers:
- name: init-permissions
image: busybox:1.37-glibc
command: ['sh', '-c', 'chown -R 1000:1000 /app/.kreuzberg']
securityContext:
runAsUser: 0
volumeMounts:
- name: cache
mountPath: /app/.kreuzberg
containers:
- name: kreuzberg
volumeMounts:
- name: cache
mountPath: /app/.kreuzberg
Health Checks¶
containers:
- name: kreuzberg
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 2
startupProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 10
failureThreshold: 30
Logging¶
Levels: trace, debug, info, warn, error
kubectl logs deployment/kreuzberg-api --tail=50
kubectl logs deployment/kreuzberg-api -f
kubectl logs deployment/kreuzberg-api --previous
Production Deployment¶
Full production manifest with namespace, PVC, security context, init container, PDB, and all probes:
apiVersion: v1
kind: Namespace
metadata:
name: kreuzberg
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: kreuzberg-cache
namespace: kreuzberg
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kreuzberg-api
namespace: kreuzberg
spec:
replicas: 3
selector:
matchLabels:
app: kreuzberg
template:
metadata:
labels:
app: kreuzberg
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: init-cache
image: busybox:1.37-glibc
command: ['sh', '-c', 'mkdir -p /app/.kreuzberg && chown -R 1000:1000 /app/.kreuzberg']
securityContext:
runAsUser: 0
volumeMounts:
- name: cache
mountPath: /app/.kreuzberg
containers:
- name: kreuzberg
image: ghcr.io/kreuzberg-dev/kreuzberg:latest
ports:
- containerPort: 8000
name: http
env:
- name: RUST_LOG
value: "info"
- name: TESSDATA_PREFIX
value: "/usr/share/tesseract-ocr/5/tessdata"
- name: KREUZBERG_CACHE_DIR
value: "/app/.kreuzberg"
- name: HF_HOME
value: "/app/.kreuzberg/huggingface"
- name: KREUZBERG_CORS_ORIGINS
value: "https://app.example.com"
- name: KREUZBERG_MAX_UPLOAD_SIZE_MB
value: "500"
args: ["serve", "--host", "0.0.0.0", "--port", "8000"]
resources:
requests:
memory: "1Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
startupProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 10
failureThreshold: 30
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: cache
mountPath: /app/.kreuzberg
- name: tmp
mountPath: /tmp
volumes:
- name: cache
persistentVolumeClaim:
claimName: kreuzberg-cache
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: kreuzberg-api
namespace: kreuzberg
spec:
type: LoadBalancer
selector:
app: kreuzberg
ports:
- protocol: TCP
port: 80
targetPort: 8000
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: kreuzberg-pdb
namespace: kreuzberg
spec:
minAvailable: 1
selector:
matchLabels:
app: kreuzberg
Model Persistence
Embedding models download on first use (~90 MB – 1.2 GB). Use a PVC for /app/.kreuzberg to avoid re-downloading on pod restart. Outside containers, models are cached in the platform-specific global cache directory (for example, ~/.cache/kreuzberg/ on Linux, ~/Library/Caches/kreuzberg/ on macOS).
High Availability¶
For HA deployments, add pod anti-affinity, rolling update strategy, and a ConfigMap for extraction settings:
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: [kreuzberg]
topologyKey: kubernetes.io/hostname
Troubleshooting¶
OCR silently failing
Verify TESSDATA_PREFIX is set and tessdata files exist:
Permission denied on cache directory
Use an init container or fsGroup (see Permissions).
OOMKilled
Increase memory limits. Reduce OCR resource usage with KREUZBERG_PDF_DPI=150 and single-language OCR.
Startup probe timeout
Increase failureThreshold on the startup probe (e.g., 60 for 10-minute timeout).
Language not found
Check installed languages with kubectl exec -it deployment/kreuzberg-api -- tesseract --list-langs. Mount custom tessdata via ConfigMap or PVC.
Diagnostic Commands¶
kubectl logs deployment/kreuzberg-api --tail=200
kubectl describe deployment kreuzberg-api
kubectl get events -n kreuzberg
kubectl exec -it deployment/kreuzberg-api -- env | sort
kubectl port-forward service/kreuzberg-api 8000:8000 && curl http://localhost:8000/health
Next Steps¶
- Docker Deployment — container configuration and image variants
- API Server Guide — endpoint documentation
- OCR Guide — backend installation and language setup
- Configuration — all configuration options