531 lines
14 KiB
Markdown
531 lines
14 KiB
Markdown
# Kubernetes Deployment <span class="version-badge new">v4.2.2</span>
|
||
|
||
Deploy Kreuzberg to Kubernetes with proper OCR configuration, permissions, and health checks.
|
||
|
||
## Helm Chart <span class="version-badge new">v4.8.4</span>
|
||
|
||
Deploy via the official Helm chart (OCI artifact on GHCR).
|
||
|
||
### Install
|
||
|
||
```bash title="Terminal"
|
||
helm install kreuzberg oci://ghcr.io/kreuzberg-dev/charts/kreuzberg --version 4.8.4
|
||
```
|
||
|
||
### Configure
|
||
|
||
Override defaults with a `values.yaml` file:
|
||
|
||
```yaml title="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
|
||
```
|
||
|
||
```bash title="Terminal"
|
||
helm install kreuzberg oci://ghcr.io/kreuzberg-dev/charts/kreuzberg \
|
||
--version 4.8.4 \
|
||
-f values.yaml
|
||
```
|
||
|
||
### Upgrade
|
||
|
||
```bash title="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`](https://github.com/kreuzberg-dev/kreuzberg/blob/main/charts/kreuzberg/values.yaml).
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
```yaml title="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
|
||
```
|
||
|
||
```bash title="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/`.
|
||
|
||
```yaml
|
||
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
|
||
|
||
```bash title="Terminal"
|
||
kubectl create configmap tessdata \
|
||
--from-file=/path/to/eng.traineddata \
|
||
--from-file=/path/to/deu.traineddata
|
||
```
|
||
|
||
```yaml
|
||
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
|
||
|
||
```bash title="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:
|
||
|
||
=== "Init Container"
|
||
|
||
```yaml
|
||
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
|
||
```
|
||
|
||
=== "fsGroup"
|
||
|
||
```yaml
|
||
spec:
|
||
securityContext:
|
||
fsGroup: 1000
|
||
containers:
|
||
- name: kreuzberg
|
||
securityContext:
|
||
runAsUser: 1000
|
||
runAsGroup: 1000
|
||
allowPrivilegeEscalation: false
|
||
readOnlyRootFilesystem: true
|
||
capabilities:
|
||
drop: ["ALL"]
|
||
```
|
||
|
||
## Health Checks
|
||
|
||
```yaml
|
||
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
|
||
|
||
```yaml
|
||
env:
|
||
- name: RUST_LOG
|
||
value: "kreuzberg=debug,warn"
|
||
```
|
||
|
||
Levels: `trace`, `debug`, `info`, `warn`, `error`
|
||
|
||
```bash title="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:
|
||
|
||
```yaml title="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
|
||
```
|
||
|
||
```bash title="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:
|
||
|
||
```yaml title="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
|
||
|
||
??? Question "OCR silently failing"
|
||
|
||
Verify `TESSDATA_PREFIX` is set and tessdata files exist:
|
||
|
||
```bash title="Terminal"
|
||
kubectl exec -it deployment/kreuzberg-api -- printenv TESSDATA_PREFIX
|
||
kubectl exec -it deployment/kreuzberg-api -- ls /usr/share/tesseract-ocr/5/tessdata/
|
||
```
|
||
|
||
??? Question "Permission denied on cache directory"
|
||
|
||
Use an init container or `fsGroup` (see [Permissions](#permissions)).
|
||
|
||
??? Question "OOMKilled"
|
||
|
||
Increase memory limits. Reduce OCR resource usage with `KREUZBERG_PDF_DPI=150` and single-language OCR.
|
||
|
||
??? Question "Startup probe timeout"
|
||
|
||
Increase `failureThreshold` on the startup probe (e.g., `60` for 10-minute timeout).
|
||
|
||
??? Question "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
|
||
|
||
```bash title="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
|
||
|
||
- [Docker Deployment](docker.md) — container configuration and image variants
|
||
- [API Server Guide](api-server.md) — endpoint documentation
|
||
- [OCR Guide](ocr.md) — backend installation and language setup
|
||
- [Configuration](configuration.md) — all configuration options
|