homelab-codex-ws/services/paperless-worker/docker-compose.yml
oskar 196a99ffef fix(paperless-worker): celery command bypassed manage.py + missing shared scratch dir
Two config bugs found on the already-deployed split-host OCR worker
(module 3): (1) `command: celery ...` was routed through manage.py by
the image entrypoint because it didn't start with "/" — fixed with an
absolute gosu+celery path. (2) SCRATCH_DIR (/tmp/paperless) was not
shared over NFS like data/media/consume, so tasks picked up by
worker@SOLARIA instead of worker@PIHA failed with "File not found" —
fixed by adding a paperless_scratch NFS volume/bind mount on both
sides.

Verified live on PIHA + SOLARIA: test PDFs dropped into consume/ were
split across both workers, the SOLARIA-picked task completed OCR with
zero File not found errors, test documents cleaned up afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:51:04 +02:00

124 lines
6.1 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Paperless OCR worker (SOLARIA) — KB module 3.
#
# Same image as services/paperless/ but the command is overridden to run ONLY
# a celery worker (no webserver, no consumer, no beat). It attaches to the
# broker/DB of paperless@PIHA and sees the document storage via NFS from PIHA.
# This split is NOT officially supported by paperless-ngx but is the
# maintainer-confirmed pattern (GH discussion #3900); constraints and risks
# are documented in README.md.
#
# When SOLARIA is powered down this worker simply disappears — queued tasks
# wait in Redis@PIHA (AOF-persisted) and PIHA's built-in concurrency-1 worker
# grinds slowly. When SOLARIA returns, this worker drains the queue.
services:
paperless-worker:
# MUST be the exact same tag as services/paperless/docker-compose.yml —
# shared DB schema + celery task signatures. Bump both together.
image: ghcr.io/paperless-ngx/paperless-ngx:2.14
container_name: paperless-worker
restart: unless-stopped
# Worker-only mode: the image entrypoint handles USERMAP + language
# install, then execs this instead of the full s6 service tree.
# OCR (ocrmypdf/tesseract) is CPU-bound — the win here is SOLARIA's
# 24 cores, not the GPU (stock paperless OCR does not use CUDA).
#
# MUST be an absolute path. /sbin/docker-entrypoint.sh branches on
# argv[0]: anything NOT starting with "/" is treated as a Django
# management-command name and handed to `manage.py` (`exec gosu paperless
# python3 manage.py "$@"`) — so a bare `celery ...` command fails with
# "Unknown command: 'celery'". An absolute path takes the `else exec "$@"`
# branch instead, running celery directly. `gosu paperless` in front
# replaces the implicit gosu the manage.py branch would have applied, so
# the process still runs as uid 1000 (paperless), not root — required so
# files it writes on the NFS-shared storage keep the correct owner and
# match USERMAP_UID/GID below. Verified against the image: gosu is at
# /usr/sbin/gosu, celery at /usr/local/bin/celery.
command: /usr/sbin/gosu paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --concurrency ${WORKER_CONCURRENCY:-4}
env_file:
- .env
environment:
# Broker + DB on PIHA over the fast LAN (NOT Tailscale) — same L2 as
# the NFS mounts, transfer is not the bottleneck. Broker has
# requirepass set — PAPERLESS_REDIS_PASSWORD in .env MUST equal the
# value in services/paperless/.env on PIHA.
- PAPERLESS_REDIS=redis://:${PAPERLESS_REDIS_PASSWORD}@192.168.31.5:6380
- PAPERLESS_DBHOST=192.168.31.5
- PAPERLESS_DBPORT=5434
- PAPERLESS_DBNAME=paperless
- PAPERLESS_DBUSER=paperless
# Settings below MUST mirror services/paperless — drift between the two
# stacks changes how documents get named/OCR-ed depending on which host
# picked the task.
- PAPERLESS_URL=https://paper.kapala.org
- PAPERLESS_TIME_ZONE=Europe/Warsaw
- PAPERLESS_OCR_LANGUAGES=pol
- PAPERLESS_OCR_LANGUAGE=pol+eng
# Threads used INSIDE a single OCR task; total load ~ concurrency ×
# threads. 4×4=16 threads leaves headroom on 24 cores/32 threads.
# TODO AT DEPLOY (module 5): measure on a sample before the 70k-attachment
# batch — decide whether to raise concurrency (e.g. 8) or keep headroom
# for ollama/AI workloads.
- PAPERLESS_THREADS_PER_WORKER=4
# MUST match paperless@PIHA — files on the NFS export carry numeric
# UID/GID; a mismatch means this worker cannot read/write documents.
- USERMAP_UID=1000
- USERMAP_GID=1000
volumes:
# NFS volumes from PIHA (defined below). Container paths MUST be
# identical to paperless@PIHA — task payloads and the DB carry absolute
# /usr/src/paperless/... paths.
- paperless_data:/usr/src/paperless/data
- paperless_media:/usr/src/paperless/media
- paperless_consume:/usr/src/paperless/consume
# SCRATCH_DIR (default /tmp/paperless — paperless-ngx sets it to
# tempfile.gettempdir()/"paperless" when PAPERLESS_SCRATCH_DIR is unset,
# see src/paperless/settings.py) is where paperless@PIHA stages the
# uploaded file before consuming it. The celery task payload carries
# that path as an ABSOLUTE path, and this worker opens the same path on
# its own filesystem — without a shared mount here, any task picked up
# by SOLARIA (instead of the local PIHA worker) fails with "Cannot
# consume ...: File not found". Must be NFS-mounted at the identical
# container path, same reasoning as data/media/consume above. No need
# to set PAPERLESS_SCRATCH_DIR explicitly: the default already resolves
# to /tmp/paperless on both sides.
- paperless_scratch:/tmp/paperless
healthcheck:
# Pings THIS worker through the broker — proves broker connectivity and
# a live celery process in one shot.
test: ["CMD", "celery", "--app", "paperless", "inspect", "ping", "--timeout", "10"]
interval: 60s
timeout: 15s
retries: 3
start_period: 60s
# Docker-managed NFS mounts (no /etc/fstab entry needed on SOLARIA). Docker
# mounts these lazily on container start; if PIHA is unreachable the container
# fails to start and docker retries via restart policy — acceptable, the queue
# waits on PIHA anyway. The HOST-side prerequisite is on PIHA: the /etc/exports
# entry (see README.md, "NFS export").
volumes:
paperless_data:
driver: local
driver_opts:
type: nfs
o: addr=192.168.31.5,rw,nfsvers=4.1,hard,timeo=150
device: ":/opt/homelab/data/paperless/data"
paperless_media:
driver: local
driver_opts:
type: nfs
o: addr=192.168.31.5,rw,nfsvers=4.1,hard,timeo=150
device: ":/opt/homelab/data/paperless/media"
paperless_consume:
driver: local
driver_opts:
type: nfs
o: addr=192.168.31.5,rw,nfsvers=4.1,hard,timeo=150
device: ":/opt/homelab/data/paperless/consume"
paperless_scratch:
driver: local
driver_opts:
type: nfs
o: addr=192.168.31.5,rw,nfsvers=4.1,hard,timeo=150
device: ":/opt/homelab/data/paperless/scratch"