Skip to content

Kubernetes Deployment v4.2.2

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

Helm Chart v4.8.4

Deploy via the official Helm chart (OCI artifact on GHCR).

Install

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

Configure

Override defaults with a values.yaml file:

values.yaml
# NOTE: cache.enabled=true uses ReadWriteOnce by default; keep replicaCount: 1
# with RWO storage or switch to ReadWriteMany before increasing replicas.
replicaCount: 1

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

Upgrade

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

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.


Quick Start

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: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

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

!!! Note "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

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.

Verify Tesseract

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

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
      allowPrivilegeEscalation: false
      capabilities:
        add: ["CHOWN"]
        drop: ["ALL"]
    volumeMounts:
    - name: cache
      mountPath: /app/.kreuzberg
  containers:
  - name: kreuzberg
    volumeMounts:
    - name: cache
      mountPath: /app/.kreuzberg
spec:
  securityContext:
    fsGroup: 1000
  containers:
  - name: kreuzberg
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]

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

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

Production Deployment

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
  # NOTE: PVC uses ReadWriteOnce; keep replicas: 1 with RWO storage.
  # Increase replicas only when using ReadWriteMany storage.
spec:
  replicas: 1
  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
            allowPrivilegeEscalation: false
            capabilities:
              add: ["CHOWN"]
              drop: ["ALL"]
          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
Terminal
kubectl apply -f production-deployment.yaml

!!! Note "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

Add pod anti-affinity and rolling update strategy:

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

Troubleshooting

OCR silently failing

Verify TESSDATA_PREFIX is set and tessdata files exist:

Terminal
kubectl exec -it deployment/kreuzberg-api -- printenv TESSDATA_PREFIX
kubectl exec -it deployment/kreuzberg-api -- ls /usr/share/tesseract-ocr/5/tessdata/
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

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

Next Steps

Edit this page on GitHub