homelab-codex-ws/services/paperless/docker-compose.yml

161 lines
7.7 KiB
YAML
Raw Permalink Normal View History

# Paperless-ngx service stack (PIHA) — UI + API + Postgres + Redis broker.
#
# KB module 2 (kb/phases/kb-m2-paperless.md). Heavy OCR runs on a
# SEPARATE worker on SOLARIA (services/paperless-worker/, module 3) that shares
# this stack's Redis broker, Postgres and document storage (NFS export from
# PIHA). The built-in celery worker here stays at concurrency 1 as the slow
# fallback when SOLARIA is asleep — the stock image cannot disable it anyway.
#
# EXPOSURE: LAN/Tailscale ONLY (documents are sensitive — kb-00 rule #4).
# Every published port binds to LAN_BIND_IP (192.168.31.5), never 0.0.0.0.
# Users enter through npm@PIHA (paper.kapala.org vhost); the worker on SOLARIA
# reaches Redis/Postgres over the same fast LAN, NOT Tailscale.
services:
paperless:
# Multi-arch manifest includes linux/arm64 — runs natively on the Pi 5.
# The paperless-worker on SOLARIA MUST run the exact same tag (shared DB
# schema + task signatures); bump both together.
image: ghcr.io/paperless-ngx/paperless-ngx:2.14
container_name: paperless
restart: unless-stopped
depends_on:
db:
condition: service_healthy
broker:
condition: service_healthy
# Same trick as vikunja: pin the Forgejo OIDC issuer to npm on PIHA so
# discovery resolves over the LAN instead of flaky public DNS.
extra_hosts:
- "forgejo.kapala.org:192.168.31.5"
# Secrets via env_file only (vikunja pattern): PAPERLESS_SECRET_KEY,
# PAPERLESS_DBPASS, PAPERLESS_SOCIALACCOUNT_PROVIDERS (holds the OIDC
# client secret, hence the whole JSON lives in .env).
env_file:
- .env
environment:
# Domain confirmed: paper.kapala.org (mesh-only, *.kapala.org wildcard
# cert already covers it — no new cert needed). DNS + npm vhost are
# deploy-time steps, see README Cutover checklist.
- PAPERLESS_URL=https://paper.kapala.org
- PAPERLESS_TIME_ZONE=Europe/Warsaw
# Password-protected broker (requirepass) — see PAPERLESS_REDIS_PASSWORD
# in .env. MUST match the broker's --requirepass value below.
- PAPERLESS_REDIS=redis://:${PAPERLESS_REDIS_PASSWORD}@broker:6379
- PAPERLESS_DBHOST=db
- PAPERLESS_DBNAME=paperless
- PAPERLESS_DBUSER=paperless
# OCR: Polish + English. `pol` is downloaded into the container at boot.
- PAPERLESS_OCR_LANGUAGES=pol
- PAPERLESS_OCR_LANGUAGE=pol+eng
# Fallback-only OCR on the Pi: single worker, single thread. The real
# OCR muscle is paperless-worker@SOLARIA pulling from the same queue.
- PAPERLESS_TASK_WORKERS=1
- PAPERLESS_THREADS_PER_WORKER=1
- PAPERLESS_WEBSERVER_WORKERS=1
# Files on disk (media/, data/, consume/) are owned by this numeric UID.
# MUST equal USERMAP_UID/GID of paperless-worker@SOLARIA — the NFS export
# carries numeric IDs, not names. See kb/services/paperless-worker.md.
- USERMAP_UID=1000
- USERMAP_GID=1000
# OIDC via Forgejo (django-allauth openid_connect). Provider JSON with
# client_id/secret comes from .env: PAPERLESS_SOCIALACCOUNT_PROVIDERS.
- PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect
# First Forgejo login auto-creates the matching Paperless account.
- PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=true
# No self-service local signups; local admin login stays for bootstrap.
# TODO AT DEPLOY: after OIDC login is verified working, set
# PAPERLESS_DISABLE_REGULAR_LOGIN=true + PAPERLESS_REDIRECT_LOGIN_TO_SSO=true
# (decision already made — Vikunja runs both modes in parallel today too).
- PAPERLESS_ACCOUNT_ALLOW_SIGNUPS=false
volumes:
# Bind mounts under /opt/homelab/data (runtime path convention). These
# exact directories are NFS-exported to SOLARIA for the OCR worker —
# container paths (/usr/src/paperless/...) MUST be identical on both
# hosts because the DB and task payloads carry absolute paths.
- /opt/homelab/data/paperless/data:/usr/src/paperless/data
- /opt/homelab/data/paperless/media:/usr/src/paperless/media
- /opt/homelab/data/paperless/consume:/usr/src/paperless/consume
- /opt/homelab/data/paperless/export:/usr/src/paperless/export
# SCRATCH_DIR — where paperless stages an uploaded file before
# consuming it (default /tmp/paperless; see paperless-ngx
# src/paperless/settings.py, tempfile.gettempdir()/"paperless"). The
# celery task payload carries this staged file's path as an ABSOLUTE
# path. Since the built-in worker here and the worker@SOLARIA share one
# task queue, whichever one picks up the task must be able to open that
# exact path — so, same rule as data/media/consume above, this MUST be
# NFS-exported and mounted at the identical container path on both
# hosts. Was missed when module 3 was first wired up: documents
# consumed on PIHA but OCR'd by the SOLARIA worker failed with "Cannot
# consume ...: File not found" because SOLARIA had its own local,
# empty /tmp/paperless.
- /opt/homelab/data/paperless/scratch:/tmp/paperless
ports:
# LAN-only bind; npm@PIHA proxies paper.kapala.org -> 192.168.31.5:8210.
# TODO AT DEPLOY: port 8210 free per the 2026-06-30 inventory —
# reconfirm on the live host: ss -tlnp | grep 8210
- "${LAN_BIND_IP}:8210:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
db:
image: postgres:16-alpine
container_name: paperless-db
restart: unless-stopped
env_file:
- .env
environment:
- POSTGRES_DB=paperless
- POSTGRES_USER=paperless
- TZ=Europe/Warsaw
volumes:
- paperless_pgdata:/var/lib/postgresql/data
ports:
# Published on the LAN for paperless-worker@SOLARIA (192.168.31.70).
# 5433 is taken by kb-postgres. Postgres itself still enforces
# scram-sha-256 auth — the bind is exposure control, not the only lock.
- "${LAN_BIND_IP}:5434:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U paperless -d paperless"]
interval: 10s
timeout: 5s
retries: 5
broker:
image: redis:7-alpine
container_name: paperless-broker
restart: unless-stopped
# AOF persistence is REQUIRED: when SOLARIA is offline, queued OCR tasks
# (up to the 70k-attachment backlog) live only in this Redis. Without AOF a
# broker restart would silently drop the queue — violating the "zero lost
# tasks" criterion of module 3. Do NOT set maxmemory/eviction here.
# requirepass: broker is bound to the LAN interface (trusted home network,
# but still reachable by anything on that LAN) — password auth decided
# over relying on the bind alone. Same value MUST be set as
# PAPERLESS_REDIS_PASSWORD in .env on both PIHA (this file) and
# paperless-worker@SOLARIA.
command: redis-server --appendonly yes --requirepass ${PAPERLESS_REDIS_PASSWORD}
volumes:
- paperless_redisdata:/data
ports:
# LAN-only bind for paperless-worker@SOLARIA. 6379 is taken by
# agent-system-redis.
# TODO AT DEPLOY: port 6380 free per the 2026-06-30 inventory —
# reconfirm on the live host: ss -tlnp | grep 6380
- "${LAN_BIND_IP}:6380:6379"
healthcheck:
test: ["CMD", "redis-cli", "-a", "${PAPERLESS_REDIS_PASSWORD}", "--no-auth-warning", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
# DB + broker state are host-local (NOT part of the NFS export). Document
# storage above is bind-mounted, not a named volume, because it doubles as
# the NFS export root for the SOLARIA worker.
paperless_pgdata:
paperless_redisdata: