Skip to content

Kubernetes Deployment

Deploy Kreuzberg to Kubernetes with proper OCR configuration, permissions, and health checks.

The recommended way to deploy Kreuzberg on Kubernetes is via the official Helm chart, published as an OCI artifact to GitHub Container Registry.

Terminal
helm install kreuzberg oci://ghcr.io/kreuzberg-dev/charts/kreuzberg --version 4.8.4

Override defaults with a values.yaml file:

values.yaml
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
Terminal
helm install kreuzberg oci://ghcr.io/kreuzberg-dev/charts/kreuzberg \
--version 4.8.4 \
-f values.yaml
Terminal
helm upgrade kreuzberg oci://ghcr.io/kreuzberg-dev/charts/kreuzberg --version 4.8.4

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.

minimal-deployment.yaml
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-full: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
Terminal
kubectl apply -f minimal-deployment.yaml

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

Terminal
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.

Terminal
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

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
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
env:
- name: RUST_LOG
value: "kreuzberg=debug,warn"

Levels: trace, debug, info, warn, error

Terminal
kubectl logs deployment/kreuzberg-api --tail=50
kubectl logs deployment/kreuzberg-api -f
kubectl logs deployment/kreuzberg-api --previous

Full production manifest with namespace, PVC, security context, init container, PDB, and all probes:

production-deployment.yaml
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-full: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
Terminal
kubectl apply -f production-deployment.yaml

For HA deployments, add pod anti-affinity, rolling update strategy, and a ConfigMap for extraction settings:

ha-additions.yaml
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
Terminal
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