# Paperless-ngx service stack (PIHA) — UI + API + Postgres + Redis broker. # # KB module 2 (docs/kb/modules/02-paperless-service.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 services/paperless-worker/README.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 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: