# Paperless-ngx (serwis, PIHA) Serwis dokumentów filaru KB #2 (moduł 2, `docs/kb/modules/02-paperless-service.md`): UI + API + Postgres + Redis. Always-on na PIHA. Ciężki OCR wykonuje osobny worker na SOLARIA (`services/paperless-worker/`, moduł 3) — tu zostaje tylko wolny fallback (1 worker × 1 wątek). **Paperless = archiwum REFERENCJA w hybrydzie kb-02** — jego storage jest źródłem prawdy dla KB. Backup jest OBOWIĄZKOWY (patrz niżej), nie opcjonalny. ## Stack | Kontener | Obraz | Rola | |--------------------|------------------------------------------|-----------------------------| | `paperless` | `ghcr.io/paperless-ngx/paperless-ngx:2.14` | UI+API+consumer+fallback-OCR (port 8000) | | `paperless-db` | `postgres:16-alpine` | metadane (alias `db`) | | `paperless-broker` | `redis:7-alpine` (AOF on) | kolejka zadań OCR (alias `broker`) | Wszystkie porty bindowane na `LAN_BIND_IP` (192.168.31.5), nigdy 0.0.0.0: - `8210` → web UI (za npm@PIHA, vhost `paper.kapala.org`, LAN/Tailscale only) - `5434` → Postgres dla workera@SOLARIA (5433 zajęte przez kb-postgres); auth scram - `6380` → Redis dla workera@SOLARIA (6379 zajęte przez agent-system-redis); `requirepass` ustawione — `PAPERLESS_REDIS_PASSWORD` w `.env` po obu stronach Obraz jest multi-arch (arm64 natywnie na Pi 5). Tag `2.14` MUSI być identyczny z `services/paperless-worker/docker-compose.yml` — podbijać oba naraz. ## Split OCR: serwis@PIHA + worker@SOLARIA (wynik badania) Rozproszony worker **nie jest oficjalnie wspierany** przez Paperless-ngx, ale jest wykonalny i potwierdzony przez maintainerów (GH discussion #3900): drugi host odpala ten sam obraz z `command: celery --app paperless worker` i musi widzieć **ten sam Redis, tego samego Postgresa i te same pliki**. Stąd: - storage dokumentów leży na PIHA (bind mounty `/opt/homelab/data/paperless/*`) i jest eksportowany przez **NFS po LAN** (nie Tailscale) do SOLARII; - ścieżki w kontenerze (`/usr/src/paperless/{data,media,consume}`) muszą być **identyczne** po obu stronach — payloady zadań i DB niosą ścieżki absolutne; - pliki mają właściciela **numerycznego UID 1000** (`USERMAP_UID/GID=1000` po obu stronach) — NFS przenosi numeryczne ID, nie nazwy; - wbudowany worker na PIHA (nie da się go wyłączyć w stockowym obrazie) działa jako wolny fallback, gdy SOLARIA śpi; zadania czekają w Redis (AOF włączone, restart brokera nie gubi kolejki). Szczegóły NFS (export na PIHA, mount na SOLARIA, ryzyko indeksu Whoosh) — `services/paperless-worker/README.md`. ## OIDC przez Forgejo Mechanizm: `django-allauth` z providerem `openid_connect` (`PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect`), konfiguracja providera w JSON-ie `PAPERLESS_SOCIALACCOUNT_PROVIDERS` (w `.env`, bo zawiera client secret — patrz `env.example`). Rejestracja w Forgejo (Settings → Applications, wzorzec jak Vikunja): - Redirect URI: `https://paper.kapala.org/accounts/oidc/forgejo/login/callback/` - Confidential client; scope `openid profile email` `forgejo.kapala.org` jest przypięte przez `extra_hosts` do `192.168.31.5` (npm na PIHA), żeby OIDC discovery szło po LAN — ta sama lekcja co w Vikunji. Lokalny login zostaje na czas bootstrapu (konto `PAPERLESS_ADMIN_USER`); po zweryfikowaniu OIDC ustawić `PAPERLESS_DISABLE_REGULAR_LOGIN=true` + `PAPERLESS_REDIRECT_LOGIN_TO_SSO=true` (decyzja podjęta — zrobić PRZY DEPLOYU, po potwierdzeniu logowania OIDC; TODO w compose). ## Storage i backup (WARUNEK BRZEGOWY hybrydy) Dane na PIHA NVMe: - `/opt/homelab/data/paperless/media` — **oryginały + zarchiwizowane PDF-y, niezastępowalne** - `/opt/homelab/data/paperless/data` — indeks wyszukiwania (Whoosh) + klasyfikator — odtwarzalne (`document_index reindex`) - `/opt/homelab/data/paperless/consume` — katalog zrzutu do ingestu - `paperless_pgdata` (named volume) — metadane (tagi, korespondenci, daty) - `paperless_redisdata` (named volume) — AOF kolejki zadań ### Backup (zatwierdzone — cel: SOLARIA, LAN; offsite później) 1. **Warstwa 1 — eksport spójny**: nocny cron na PIHA: `docker exec paperless document_exporter ../export --delete -z` — wbudowane narzędzie; zrzuca oryginały + archiwa + `manifest.json` (pełne metadane, odtwarzalne przez `document_importer` bez dumpa SQL). Ląduje w `/opt/homelab/data/paperless/export`. 2. **Warstwa 2 — kopia poza hosta**: rsync/borg eksportu na SOLARIA (2 TB NVMe, ta sama LAN) — SOLARIA sesyjna, więc pull przy starcie lub push z retry. Wystarcza na start; VPS odpada (brak miejsca). 3. **Retencja (zatwierdzona)**: 7 dziennych + 4 tygodniowe + 6 miesięcznych. 4. Dump Postgresa nie jest konieczny do odtworzenia (manifest wystarcza), ale tani — można dołożyć `pg_dump` do tej samej nocnej pętli. 5. **Future-note (poza zakresem tego etapu)**: offsite backup (np. restic → chmura) poza SOLARIA-LAN, gdy pojawi się potrzeba/budżet. Ten etap to tylko config + dokumentacja decyzji — cron/skrypt deployowy (cron entry na PIHA, rsync/borg do SOLARII) powstaje przy deployu modułu 2, nie tutaj. ## RAM na PIHA Oczekiwany spoczynek: paperless ~400–600 Mi (gunicorn + consumer + beat + 1 worker), postgres ~80 Mi, redis ~15 Mi. Limity twarde w `hosts/piha/runtime/paperless/docker-compose.override.yml`. Warunek: moduł 0 (odchudzenie PIHA) wykonany — po nim ~3.8 Gi available. ## Cutover checklist (przy deployu — NIE teraz) 1. Moduł 0 (piha-slim) domknięty; `free -g` na PIHA potwierdza zapas. 2. Porty wolne na żywym hoście: `ss -tlnp | grep -E '8210|5434|6380'`. 3. `mkdir -p /opt/homelab/data/paperless/{data,media,consume,export}` na PIHA. 4. `.env` z `env.example` (sekrety + rejestracja OAuth2 w Forgejo). 5. DNS: rekord A `paper.kapala.org` w Cloudflare → `100.108.208.3` (Tailscale PIHA, DNS Only) — ten sam wzorzec co `ha.kapala.org` / `immich.kapala.org` (`docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md`); wildcard `*.kapala.org` już pokrywa tę subdomenę, nowy cert niepotrzebny. Plus vhost w npm@PIHA (HTTPS → `192.168.31.5:8210`, Advanced puste). 6. `docker compose up -d` + `./healthcheck.sh` + testowy login OIDC. 7. Export NFS dla workera (patrz `services/paperless-worker/README.md`) — dopiero przy module 3. 8. Wpis w `hosts/piha/services.yaml` (dopiero przy deployu — wcześniej supervisor widziałby drift dla nieistniejącego serwisu).