# 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"