diff --git a/kubernetes/apps/paperless/paperless.yaml b/kubernetes/apps/paperless/paperless.yaml index 0247cfd9..85ae85e4 100644 --- a/kubernetes/apps/paperless/paperless.yaml +++ b/kubernetes/apps/paperless/paperless.yaml @@ -27,6 +27,24 @@ data: PAPERLESS_ALLOWED_HOSTS: "*" PAPERLESS_TIME_ZONE: "Europe/Berlin" PAPERLESS_OCR_LANGUAGE: "deu+eng" + # OCR concurrency, pinned rather than derived. paperless hands + # THREADS_PER_WORKER to ocrmypdf as its job count, and the default is + # cpu_count() / TASK_WORKERS. The container has no CPU limit, so + # multiprocessing.cpu_count() reports the NODE's cores — 20 on k8s-1 — + # and OCR fans out to "Starting processing with 20 workers concurrently". + # Twenty pages of a 300dpi colour scan rasterised at once blew past + # limits.memory below and the kernel OOMKilled the container (exit 137) + # mid-consume. Because celery re-picks the same document on every restart, + # that is a crash loop and not a one-off: the pod OOMs ~37s in, before the + # webserver ever finishes booting, so it never goes Ready and the archive + # stays down. + # + # TASK_WORKERS is already effectively 1 (20 cores / 1 worker = the 20 jobs + # observed); pinning it keeps that explicit so THREADS_PER_WORKER stays the + # only knob, and neither value silently changes when this lands on a node + # with a different core count. + PAPERLESS_TASK_WORKERS: "1" + PAPERLESS_THREADS_PER_WORKER: "4" PAPERLESS_CONSUMER_RECURSIVE: "true" PAPERLESS_CONSUMER_DELETE_DUPLICATES: "true" PAPERLESS_CONSUMER_ENABLE_BARCODES: "true" @@ -273,7 +291,15 @@ spec: cpu: 200m memory: 512Mi limits: - memory: 2Gi + # Headroom, not the fix — capping OCR jobs in paperless-config + # is. Sized from what a healthy pod actually uses: the last + # long-lived container held a steady ~1.0Gi working set, so 2Gi + # left barely 1Gi for an OCR burst. 3Gi keeps a 4-job burst + # clear of the ceiling. The crashlooping containers never showed + # this in cadvisor (~820Mi peak) because they lived 37s against + # a ~30s scrape interval — the spike fell between samples, so + # the OOMKilled status, not the metric, is the reliable signal. + memory: 3Gi securityContext: runAsNonRoot: false allowPrivilegeEscalation: true