feat(kb): configi Paperless (PIHA) + OCR-worker (SOLARIA, NFS split-host) + Nextcloud — do review, split-host NFS zweryfikowany (GH #3900), 9 decyzji w DECYZJE-do-podjecia.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
670fa7a5e4
commit
2aa47963b3
103
docs/kb/modules/DECYZJE-do-podjecia.md
Normal file
103
docs/kb/modules/DECYZJE-do-podjecia.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Decyzje do podjęcia — filar dokumentów (moduły 2/3/4)
|
||||
|
||||
> Zbiorcza lista otwartych decyzji z przygotowania configów (2026-07-06,
|
||||
> branch `task/paperless-nextcloud-config`). Każda pozycja ma odpowiadający
|
||||
> komentarz `# TODO DECYZJA OSKARA:` w plikach serwisów. Configi są gotowe do
|
||||
> review — NIC nie zostało zdeployowane.
|
||||
|
||||
## 1. Host Nextclouda: PIHA czy SOLARIA
|
||||
|
||||
Jedyna duża decyzja architektoniczna. Moduł 4 i kb-02 skłaniają się ku
|
||||
**SOLARIA** (KB czyta własną kopię z archiwum na PIHA, więc dostępność
|
||||
Nextclouda nie warunkuje zapytań KB; koszt = sync dogania się po wybudzeniu).
|
||||
Tabela za/przeciw: `services/nextcloud/README.md`. Compose jest przenośne —
|
||||
decyzja ustawia tylko `owner_node` w `service.yaml` + `LAN_BIND_IP`
|
||||
i `TRUSTED_PROXIES` w `.env`. Wstępnie wpisane: SOLARIA.
|
||||
|
||||
## 2. Backup Paperlessa (warunek brzegowy hybrydy — musi być przed produkcyjnym użyciem)
|
||||
|
||||
Propozycja w `services/paperless/README.md`:
|
||||
|
||||
- nocny `document_exporter` (oryginały + archiwa + manifest metadanych,
|
||||
odtwarzalny bez dumpa SQL) do `/opt/homelab/data/paperless/export`,
|
||||
- kopia eksportu poza hosta: **rsync/borg → SOLARIA** (2 TB, ta sama LAN);
|
||||
otwarte: czy dokładać offsite (restic → chmura?),
|
||||
- retencja (propozycja): 7 dziennych + 4 tygodniowe + 6 miesięcznych.
|
||||
|
||||
Do zatwierdzenia: cel kopii (SOLARIA wystarczy? offsite?), retencja, narzędzie.
|
||||
|
||||
## 3. Porty (potwierdzić na żywym PIHA przed deployem)
|
||||
|
||||
Dobrane wg inwentaryzacji 2026-06-30 (snapshot! zweryfikować `ss -tlnp`):
|
||||
|
||||
| Port | Serwis | Uwagi |
|
||||
|---|---|---|
|
||||
| 8210 | paperless web | za npm@PIHA |
|
||||
| 5434 | paperless postgres | 5433 zajęte przez kb-postgres |
|
||||
| 6380 | paperless redis (broker dla workera) | 6379 zajęte przez agent-system-redis |
|
||||
| 8220 | nextcloud web | wolny i na PIHA, i na SOLARII |
|
||||
|
||||
Wszystkie bindowane na LAN IP (nie 0.0.0.0); ruch worker↔broker/DB/NFS po
|
||||
LAN 192.168.31.x, nie Tailscale.
|
||||
|
||||
## 4. Redis brokera bez auth na bindzie LAN
|
||||
|
||||
Broker (6380) i Postgres (5434) są wystawione na LAN IP PIHA dla workera na
|
||||
SOLARII. Postgres ma auth (scram); **Redis defaultowo nie ma**. Zaufany LAN
|
||||
domowy — akceptować, czy dołożyć `requirepass` (wtedy `PAPERLESS_REDIS`
|
||||
z hasłem wędruje do `.env` po obu stronach)?
|
||||
|
||||
## 5. Fallback-worker na PIHA a indeks Whoosh po NFS
|
||||
|
||||
Stockowy obraz na PIHA zawsze ma wbudowany worker (concurrency 1) — to
|
||||
naturalny fallback „domiela wolno". Ryzyko: gdy SOLARIA pracuje, **dwa hosty
|
||||
piszą do plikowego indeksu Whoosh** (PIHA lokalnie, SOLARIA po NFS) — znane
|
||||
upstreamowe objawy wyścigu („This writer is closed", korupcja indeksu).
|
||||
Indeks jest odtwarzalny (`document_index reindex`), oryginałom nic nie grozi.
|
||||
Decyzja: zaakceptować i obserwować (rekomendacja), czy od razu wymusić
|
||||
tryb „kolejka-czeka" (batch-OCR tylko w oknach pracy SOLARII)?
|
||||
Szczegóły: `services/paperless-worker/README.md`.
|
||||
|
||||
## 6. Domeny + wpisy DNS + vhosty npm
|
||||
|
||||
Założone w configach: `paper.kapala.org` (Paperless), `cloud.kapala.org`
|
||||
(Nextcloud) — moduły dopuszczały też `*.okit.pl`. Potwierdzić, potem:
|
||||
A-recordy (DNS Only) → PIHA + vhosty w npm@PIHA + rejestracja OAuth2 apps
|
||||
w Forgejo (redirect URIs w README serwisów zależą od domeny).
|
||||
|
||||
## 7. Wyłączenie lokalnego loginu w Paperless po weryfikacji OIDC
|
||||
|
||||
Bootstrap idzie przez lokalnego admina (`PAPERLESS_ADMIN_USER`). Po
|
||||
potwierdzeniu działania Forgejo-OIDC: ustawić
|
||||
`PAPERLESS_DISABLE_REGULAR_LOGIN=true` + `PAPERLESS_REDIRECT_LOGIN_TO_SSO=true`?
|
||||
(Vikunja ma dziś oba tryby równolegle.)
|
||||
|
||||
## 8. Pin wersji Nextclouda
|
||||
|
||||
Compose ma ruchomy `nextcloud:stable-apache`; przy deployu przypiąć aktualny
|
||||
major (np. `:31-apache` — sprawdzić bieżący stable w dniu deployu). Nextcloud
|
||||
nie wspiera skoków o >1 wersję major, więc ruchomy tag na produkcji = ryzyko
|
||||
niekontrolowanego skoku.
|
||||
|
||||
## 9. Sizing workera OCR pod batch 70k załączników
|
||||
|
||||
`WORKER_CONCURRENCY=4` × `PAPERLESS_THREADS_PER_WORKER=4` = ~16 wątków na
|
||||
24 rdzeniach SOLARII. Przed batchem 70k (moduł 5): zmierzyć na próbce i
|
||||
zdecydować, czy podbić concurrency, czy zostawić zapas na ollama/AI.
|
||||
|
||||
---
|
||||
|
||||
## Rozstrzygnięte w tym przygotowaniu (dla porządku)
|
||||
|
||||
- **Split-host OCR-worker przez NFS: WYKONALNY** — wzorzec potwierdzony przez
|
||||
maintainerów paperless-ngx (nieoficjalnie wspierany): ten sam obraz,
|
||||
`command: celery --app paperless worker`, wspólny Redis+Postgres+storage,
|
||||
identyczne ścieżki kontenerowe i numeryczny UID po obu stronach. Pełny
|
||||
wynik badania + ryzyka: `services/paperless-worker/README.md`.
|
||||
- Storage dokumentów na PIHA; NFS export → SOLARIA po LAN
|
||||
(192.168.31.5 → 192.168.31.70), nie Tailscale.
|
||||
- AOF w Redis brokera (kolejka przeżywa restart — zero utraty zadań).
|
||||
- OIDC: Paperless przez `django-allauth openid_connect` (env),
|
||||
Nextcloud przez appkę `user_oidc` (kroki `occ` w README).
|
||||
- Limity RAM na PIHA: `hosts/piha/runtime/paperless/docker-compose.override.yml`
|
||||
(stack ≤ ~1.9 Gi worst-case).
|
||||
41
hosts/piha/runtime/paperless/docker-compose.override.yml
Normal file
41
hosts/piha/runtime/paperless/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# PIHA-specific overrides for paperless (KB module 2).
|
||||
#
|
||||
# RESOURCE CONTEXT: PIHA is the RAM-bound 8 GB box shared with Home Assistant
|
||||
# and monitoring. Precondition for this service is KB module 0 (piha-slim);
|
||||
# after it ~3.8 Gi should be available. Ceilings below keep the whole stack
|
||||
# ≤ ~1.9 Gi worst-case so paperless can never starve HA — the cgroup OOM
|
||||
# killer restarts the offending container instead of the host killer picking
|
||||
# a victim.
|
||||
services:
|
||||
paperless:
|
||||
# Idle expectation ~400–600 Mi (gunicorn + consumer + beat + 1 celery
|
||||
# worker). The ceiling leaves room for the concurrency-1 FALLBACK OCR of
|
||||
# a large PDF when SOLARIA is asleep; a pathological document gets the
|
||||
# container restarted rather than the host swapping.
|
||||
mem_limit: 1400m
|
||||
mem_reservation: 512m
|
||||
|
||||
db:
|
||||
# Metadata-only DB (documents live on disk, not in Postgres). Same tuning
|
||||
# philosophy as kb-postgres but smaller: bounded backends because the
|
||||
# SOLARIA worker also holds connections (concurrency 4 + webserver pool).
|
||||
mem_limit: 384m
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "shared_buffers=96MB"
|
||||
- "-c"
|
||||
- "effective_cache_size=256MB"
|
||||
- "-c"
|
||||
- "work_mem=4MB"
|
||||
- "-c"
|
||||
- "maintenance_work_mem=32MB"
|
||||
- "-c"
|
||||
- "max_connections=50"
|
||||
|
||||
broker:
|
||||
# Task payloads are tiny (paths + ids): even the 70k-attachment backlog
|
||||
# is ~tens of MB in-queue. NO maxmemory/eviction — evicting broker keys
|
||||
# would silently drop queued OCR tasks; if the limit is ever hit the
|
||||
# container restarts and recovers the queue from AOF instead.
|
||||
mem_limit: 256m
|
||||
101
services/nextcloud/README.md
Normal file
101
services/nextcloud/README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Nextcloud (drive / WebDAV)
|
||||
|
||||
Drugi adapter dokumentów filaru KB #2 (moduł 4, `docs/kb/modules/04-nextcloud.md`):
|
||||
zamiennik Google Drive — dowolne pliki + sync telefon/desktop, źródło dla
|
||||
ingestu KB (moduł 5) przez WebDAV.
|
||||
|
||||
**Nextcloud = archiwum KOPIA w hybrydzie kb-02** — ingest robi snapshot pliku
|
||||
do archiwum KB; Nextcloud NIE jest źródłem prawdy (inaczej niż Paperless).
|
||||
Backup „warto" (dane użytkownika), ale nie jest warunkiem brzegowym KB.
|
||||
|
||||
## TODO DECYZJA OSKARA: host (PIHA vs SOLARIA)
|
||||
|
||||
Otwarte w kb-02 i module 4. Compose jest przenośne (ścieżki po konwencji
|
||||
`/opt/homelab/data`, bind IP i proxy w `.env`) — decyzja wybiera node
|
||||
i dwie wartości w `.env`.
|
||||
|
||||
| | PIHA | SOLARIA |
|
||||
|---|---|---|
|
||||
| Dostępność | 24/7 (sync zawsze działa) | sesyjna — sync dogania się po wybudzeniu |
|
||||
| RAM/CPU | ciasno nawet po module 0 (Nextcloud+PHP ≈ 0.5–1 Gi+) | 62 Gi RAM, 24 rdzenie — bez znaczenia |
|
||||
| Storage | NVMe 477 G (dzielone z resztą) | NVMe 2 T |
|
||||
| Ingress | npm lokalnie | npm@PIHA proxuje po LAN do 192.168.31.70:8220 (npm proxuje na dowolny IP — bez przeszkód) |
|
||||
| Wpływ na KB | żaden — KB czyta własną kopię z archiwum na PIHA w obu wariantach | jw. |
|
||||
|
||||
Skłonność modułu 4: **SOLARIA** (dlatego `service.yaml` ma wstępnie
|
||||
`owner_node: solaria`, a `env.example` IP SOLARII). Koszt: przerwy w sync,
|
||||
gdy host śpi — do zaakceptowania, sync się dogoni.
|
||||
|
||||
## Stack
|
||||
|
||||
| Kontener | Obraz | Rola |
|
||||
|------------------|-------------------------|---------------------------------------|
|
||||
| `nextcloud` | `nextcloud:stable-apache` | app + WebDAV (port 80 → host 8220 na LAN_BIND_IP) |
|
||||
| `nextcloud-cron` | `nextcloud:stable-apache` | joby w tle (`/cron.sh`, ten sam wolumen) |
|
||||
| `nextcloud-db` | `postgres:16-alpine` | baza (bez portu na hoście) |
|
||||
| `nextcloud-redis`| `redis:7-alpine` | cache + file locking (bez portu, bez persystencji) |
|
||||
|
||||
TODO DECYZJA OSKARA: przy deployu przypiąć konkretną wersję major
|
||||
(np. `nextcloud:31-apache`) zamiast ruchomego `stable` — Nextcloud nie
|
||||
wspiera skoków o więcej niż jedną wersję major przy upgrade.
|
||||
|
||||
## OIDC przez Forgejo (krok po-deployowy, occ)
|
||||
|
||||
Mechanizm: oficjalna appka **`user_oidc`** (utrzymywana przez Nextcloud GmbH —
|
||||
wybieramy ją zamiast community `sociallogin`). Konfiguruje się ją przez `occ`
|
||||
po pierwszym starcie — NIE przez env, stąd kroki w checkliście:
|
||||
|
||||
```bash
|
||||
# w kontenerze nextcloud, jako www-data:
|
||||
docker exec -u www-data nextcloud php occ app:install user_oidc
|
||||
docker exec -u www-data nextcloud php occ user_oidc:provider forgejo \
|
||||
--clientid="<CLIENT_ID>" \
|
||||
--clientsecret="<CLIENT_SECRET>" \
|
||||
--discoveryuri="https://forgejo.kapala.org/.well-known/openid-configuration" \
|
||||
--scope="openid profile email" \
|
||||
--unique-uid=0 \
|
||||
--mapping-display-name=name --mapping-email=email --mapping-uid=preferred_username
|
||||
```
|
||||
|
||||
Rejestracja w Forgejo (Settings → Applications, wzorzec jak Vikunja):
|
||||
|
||||
- Redirect URI: `https://cloud.kapala.org/apps/user_oidc/code`
|
||||
- Confidential client; scope `openid profile email`
|
||||
|
||||
`forgejo.kapala.org` jest przypięte w compose przez `extra_hosts` do
|
||||
`192.168.31.5` (npm@PIHA) — OIDC discovery po LAN, lekcja z Vikunji.
|
||||
`--unique-uid=0` + mapping `preferred_username` daje czytelne loginy
|
||||
(np. `oskar`) zamiast hashowanych ID — istotne dla WebDAV-owych URL-i.
|
||||
|
||||
## WebDAV dla ingestu (moduł 5)
|
||||
|
||||
Endpoint: `https://cloud.kapala.org/remote.php/dav/files/<user>/`.
|
||||
Konta OIDC nie mają hasła — dla ingestu wygenerować **app password**
|
||||
(Settings → Security → Devices & sessions) i trzymać je w sekretach
|
||||
adaptera ingest, nie w tym repo.
|
||||
|
||||
## Storage i backup
|
||||
|
||||
- `/opt/homelab/data/nextcloud/html` — aplikacja + config + **pliki
|
||||
użytkowników** (`html/data/`)
|
||||
- `/opt/homelab/data/nextcloud/db` — Postgres
|
||||
|
||||
TODO DECYZJA OSKARA: backup user-data (mniej krytyczny niż Paperless, bo
|
||||
KB trzyma kopie zaingestowanych plików): propozycja — rsync/borg
|
||||
`html/data` + `pg_dump` w tej samej nocnej pętli co backup Paperlessa,
|
||||
retencja krótsza (np. 7 dziennych + 4 tygodniowe).
|
||||
|
||||
## Cutover checklist (przy deployu — NIE teraz)
|
||||
|
||||
1. Decyzja hosta ↑ podjęta; `LAN_BIND_IP`/`TRUSTED_PROXIES` w `.env` pod nią.
|
||||
2. Port wolny na żywym hoście: `ss -tlnp | grep 8220`.
|
||||
3. `mkdir -p /opt/homelab/data/nextcloud/{html,db}` na wybranym node.
|
||||
4. `.env` z `env.example`.
|
||||
5. DNS `cloud.kapala.org` → PIHA (DNS Only) + vhost w npm@PIHA (HTTPS →
|
||||
`<LAN_BIND_IP>:8220`; jeśli host=SOLARIA, target = 192.168.31.70).
|
||||
6. `docker compose up -d`; pierwszy start instaluje NC (2–3 min),
|
||||
potem `./healthcheck.sh`.
|
||||
7. Kroki `occ` dla `user_oidc` (wyżej) + rejestracja appki w Forgejo +
|
||||
testowy login OIDC.
|
||||
8. Test sync klientem (telefon) + test WebDAV (`curl -u user:app-password`).
|
||||
9. Wpis w `hosts/<node>/services.yaml` + topology (dopiero przy deployu).
|
||||
123
services/nextcloud/docker-compose.yml
Normal file
123
services/nextcloud/docker-compose.yml
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Nextcloud (self-hosted Drive, WebDAV) — KB module 4.
|
||||
#
|
||||
# Second document source for KB pillar #2: arbitrary files + phone/desktop
|
||||
# sync. KB ingest (module 5) SNAPSHOTS files into the KB archive (Nextcloud =
|
||||
# KOPIA in the kb-02 hybrid) — Nextcloud is NOT a source of truth, which
|
||||
# relaxes its backup and availability requirements vs paperless.
|
||||
#
|
||||
# TODO DECYZJA OSKARA: host — PIHA (always-on, ale ciasno) czy SOLARIA
|
||||
# (mocna, sesyjna; sync dogania sie po wybudzeniu)? Modul 4 sklania sie ku
|
||||
# SOLARIA. Compose jest przenosne: wszystkie sciezki po konwencji
|
||||
# /opt/homelab/data, bind IP i proxy z .env — decyzja wybiera tylko node
|
||||
# i wartosci w .env.
|
||||
#
|
||||
# EXPOSURE: LAN/Tailscale only przez npm@PIHA (cloud.kapala.org), zero public.
|
||||
# Jesli host=SOLARIA, npm@PIHA proxuje po LAN do 192.168.31.70:8220.
|
||||
services:
|
||||
nextcloud:
|
||||
# Multi-arch (arm64 + amd64) — runs on either candidate host.
|
||||
# TODO DECYZJA OSKARA: przy deployu przypiac konkretna wersje major
|
||||
# (np. nextcloud:31-apache) zamiast ruchomego "stable" — Nextcloud NIE
|
||||
# wspiera przeskakiwania wersji major przy upgrade.
|
||||
image: nextcloud:stable-apache
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
# OIDC discovery against Forgejo over the LAN (same lesson as vikunja).
|
||||
extra_hosts:
|
||||
- "forgejo.kapala.org:192.168.31.5"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_DB=nextcloud
|
||||
- POSTGRES_USER=nextcloud
|
||||
# Redis: PHP session locking + file locking cache.
|
||||
- REDIS_HOST=redis
|
||||
# TODO DECYZJA OSKARA: potwierdzic domene cloud.kapala.org + DNS + vhost npm.
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=cloud.kapala.org
|
||||
# Behind npm@PIHA (TLS terminated there); without these Nextcloud
|
||||
# generates http:// links and login loops.
|
||||
- OVERWRITEPROTOCOL=https
|
||||
- OVERWRITEHOST=cloud.kapala.org
|
||||
- OVERWRITECLIURL=https://cloud.kapala.org
|
||||
# npm@PIHA as seen by this container: LAN IP of PIHA when Nextcloud
|
||||
# runs on SOLARIA; docker bridge subnet when it runs on PIHA itself —
|
||||
# hence configurable via .env.
|
||||
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
|
||||
- PHP_MEMORY_LIMIT=512M
|
||||
- PHP_UPLOAD_LIMIT=4G
|
||||
- TZ=Europe/Warsaw
|
||||
volumes:
|
||||
# Whole app dir (code + config + user data in html/data). Runtime path
|
||||
# convention: /opt/homelab/data/<service>/ on the chosen node's NVMe.
|
||||
- /opt/homelab/data/nextcloud/html:/var/www/html
|
||||
ports:
|
||||
# Bind to the node's LAN IP only, never 0.0.0.0 — npm@PIHA is the sole
|
||||
# entry point (LAN/Tailscale). Port 8220 free on PIHA per the 2026-06-30
|
||||
# inventory; free on SOLARIA (nearly empty host).
|
||||
# TODO DECYZJA OSKARA: potwierdzic port na zywym hoscie: ss -tlnp | grep 8220
|
||||
- "${LAN_BIND_IP}:8220:80"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "--max-time", "5", "http://localhost:80/status.php"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 120s
|
||||
|
||||
# Background jobs (file scans, trash/versions cleanup, app jobs) — the
|
||||
# official image's dedicated cron entrypoint on the same code/data volume.
|
||||
cron:
|
||||
image: nextcloud:stable-apache
|
||||
container_name: nextcloud-cron
|
||||
restart: unless-stopped
|
||||
entrypoint: /cron.sh
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_DB=nextcloud
|
||||
- POSTGRES_USER=nextcloud
|
||||
- REDIS_HOST=redis
|
||||
- TZ=Europe/Warsaw
|
||||
volumes:
|
||||
- /opt/homelab/data/nextcloud/html:/var/www/html
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: nextcloud-db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- POSTGRES_DB=nextcloud
|
||||
- POSTGRES_USER=nextcloud
|
||||
- TZ=Europe/Warsaw
|
||||
volumes:
|
||||
- /opt/homelab/data/nextcloud/db:/var/lib/postgresql/data
|
||||
# No published port — only this stack talks to it.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: nextcloud-redis
|
||||
restart: unless-stopped
|
||||
# Pure cache/locking — no persistence needed, no published port.
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
21
services/nextcloud/env.example
Normal file
21
services/nextcloud/env.example
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Nextcloud secrets + host-local binds — copy to .env (gitignored) next to
|
||||
# docker-compose.yml and fill in real values. Never commit .env.
|
||||
|
||||
# LAN IP of the node Nextcloud lands on. The web port (8220) binds ONLY to
|
||||
# this interface — never 0.0.0.0.
|
||||
# TODO DECYZJA OSKARA: host! SOLARIA -> 192.168.31.70, PIHA -> 192.168.31.5.
|
||||
LAN_BIND_IP=192.168.31.70
|
||||
|
||||
# Reverse proxy (npm@PIHA) as seen from THIS node's containers:
|
||||
# - Nextcloud on SOLARIA: npm connects over the LAN -> PIHA's LAN IP.
|
||||
# - Nextcloud on PIHA: npm is a local container -> use the docker bridge
|
||||
# subnet instead (e.g. 172.16.0.0/12), verify with docker network inspect.
|
||||
TRUSTED_PROXIES=192.168.31.5
|
||||
|
||||
# Postgres password for the nextcloud DB (app reads the same var).
|
||||
POSTGRES_PASSWORD=change-me-very-strong
|
||||
|
||||
# Bootstrap admin — auto-provisioned on FIRST start only. Used to install and
|
||||
# configure the user_oidc app (see README), day-to-day login is Forgejo OIDC.
|
||||
NEXTCLOUD_ADMIN_USER=oskar
|
||||
NEXTCLOUD_ADMIN_PASSWORD=change-me-bootstrap-only
|
||||
27
services/nextcloud/healthcheck.sh
Executable file
27
services/nextcloud/healthcheck.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
# Healthcheck for Nextcloud (app + cron + db + redis)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Port is bound to the LAN interface only, so localhost won't answer.
|
||||
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/.env"
|
||||
fi
|
||||
BIND_IP="${LAN_BIND_IP:-127.0.0.1}"
|
||||
|
||||
for c in nextcloud nextcloud-cron nextcloud-db nextcloud-redis; do
|
||||
if ! docker ps --filter "name=$c" --filter "status=running" | grep -qw "$c"; then
|
||||
echo "[FAIL] $c container is not running"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# status.php must report installed:true (also proves DB connectivity)
|
||||
if ! curl -sf --max-time 10 "http://${BIND_IP}:8220/status.php" | grep -q '"installed":true'; then
|
||||
echo "[FAIL] Nextcloud status.php not healthy on ${BIND_IP}:8220"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[OK] nextcloud is healthy"
|
||||
exit 0
|
||||
38
services/nextcloud/service.yaml
Normal file
38
services/nextcloud/service.yaml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
service:
|
||||
name: nextcloud
|
||||
# TODO DECYZJA OSKARA: owner_node — piha (always-on, ciasno) vs solaria
|
||||
# (mocna, sesyjna). Modul 4 / kb-02 sklaniaja sie ku SOLARIA: KB-kopia
|
||||
# dokumentow i tak lezy w archiwum na PIHA, wiec zapytania KB dzialaja bez
|
||||
# Nextclouda; sync z telefonu dogania sie po wybudzeniu hosta.
|
||||
owner_node: solaria
|
||||
role: file-sync-drive # Drive replacement + WebDAV source for KB ingest (archiwum = KOPIA)
|
||||
exposure: private # LAN/Tailscale only via npm@PIHA (cloud.kapala.org); NO public ingress
|
||||
dependencies:
|
||||
- forgejo # OIDC identity provider (forgejo.kapala.org), app user_oidc
|
||||
ports:
|
||||
- container: 80
|
||||
host: 8220 # LAN_BIND_IP only, never 0.0.0.0
|
||||
protocol: tcp
|
||||
healthcheck:
|
||||
type: http
|
||||
endpoint: http://localhost:8220/status.php # use the node's LAN bind IP — localhost does not answer
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
restart_policy: unless-stopped
|
||||
persistence:
|
||||
# Nextcloud = KOPIA in the kb-02 hybrid: KB snapshots files at ingest, so
|
||||
# this data is NOT the KB source of truth. Backup is "warto" (user data),
|
||||
# not a KB boundary condition like paperless.
|
||||
paths:
|
||||
- /opt/homelab/data/nextcloud/html # app + config + user files (html/data/)
|
||||
- /opt/homelab/data/nextcloud/db # postgres data
|
||||
runtime:
|
||||
config_files:
|
||||
- .env # secrets (gitignored, from env.example)
|
||||
env_vars:
|
||||
- LAN_BIND_IP # required — LAN IP of the CHOSEN host node
|
||||
- TRUSTED_PROXIES # npm@PIHA as seen from this node
|
||||
- POSTGRES_PASSWORD
|
||||
- NEXTCLOUD_ADMIN_USER # bootstrap admin (before OIDC is wired)
|
||||
- NEXTCLOUD_ADMIN_PASSWORD
|
||||
100
services/paperless-worker/README.md
Normal file
100
services/paperless-worker/README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Paperless OCR worker (SOLARIA)
|
||||
|
||||
Ciężki OCR filaru KB #2 (moduł 3, `docs/kb/modules/03-paperless-ocr-worker.md`).
|
||||
Ten sam obraz co `services/paperless/`, ale z nadpisanym poleceniem — działa
|
||||
**wyłącznie** jako celery worker podpięty do brokera/bazy paperless@PIHA,
|
||||
ze storage widzianym przez NFS z PIHA. Realizuje wzorzec kb-02
|
||||
„serwis always-on + compute-worker on-demand z fallback".
|
||||
|
||||
## Wynik badania: czy split-host worker jest wykonalny?
|
||||
|
||||
**TAK — z zastrzeżeniami.** Ustalenia (GH discussion #3900 + docs):
|
||||
|
||||
1. **Nie jest to oficjalnie wspierane** przez Paperless-ngx, ale maintainerzy
|
||||
potwierdzają wykonalność i podają dokładnie ten wzorzec: drugi host, ten sam
|
||||
obraz, `command: celery --app paperless worker --loglevel INFO`, dostęp
|
||||
sieciowy do Redis + Postgres + współdzielonych plików.
|
||||
2. Worker potrzebuje **pełnego kontekstu aplikacji**: broker (Redis), baza
|
||||
(Postgres) i **te same pliki pod tymi samymi ścieżkami kontenerowymi**
|
||||
(`/usr/src/paperless/{data,media,consume}`) — payloady zadań i DB niosą
|
||||
ścieżki absolutne.
|
||||
3. **Podział pracy**: consumer (inotify na `consume/`), beat (harmonogram)
|
||||
i webserver zostają na PIHA; na SOLARIA idzie wyłącznie egzekucja zadań
|
||||
celery (OCR/parsowanie/archiwizacja). Consumer na PIHA działa na lokalnym
|
||||
dysku, więc inotify działa normalnie (ograniczenie „inotify nie działa po
|
||||
NFS" nas nie dotyka — po NFS czyta tylko worker, nie consumer).
|
||||
4. **Fallback**: obu-stronny worker na tej samej kolejce. Gdy SOLARIA śpi —
|
||||
zadania czekają w Redis (AOF na PIHA) i wbudowany worker PIHA
|
||||
(concurrency 1) mieli powoli. Gdy SOLARIA wstaje — worker z concurrency 4+
|
||||
rozładowuje kolejkę. Gdy worker zniknie w połowie zadania, celery na
|
||||
transporcie redis odda nieodebrane zadanie po visibility timeout
|
||||
(domyślnie 1 h) — zadanie nie ginie, najwyżej się opóźnia.
|
||||
5. **GPU nie gra roli** — stockowy OCR (ocrmypdf/tesseract) jest CPU-only.
|
||||
Zysk SOLARII to 24 rdzenie i 62 Gi RAM, nie CUDA.
|
||||
|
||||
### Znane ryzyka (świadomie zaakceptowane / do obserwacji)
|
||||
|
||||
- **Indeks Whoosh w `data/` przy dwóch piszących hostach.** Po konsumpcji
|
||||
dokumentu worker dopisuje go do indeksu wyszukiwania (plikowy Whoosh
|
||||
w `data/index`). Przy workerze na PIHA (lokalnie) i na SOLARIA (po NFS)
|
||||
równolegle istnieje ryzyko wyścigu na lockach plikowych (znane objawy
|
||||
w upstream: „This writer is closed", skorumpowany TOC indeksu). Mitygacja:
|
||||
indeks to dane **pochodne** — `docker exec paperless document_index reindex`
|
||||
odtwarza go w całości; oryginały i metadane nie są zagrożone. Jeśli
|
||||
korupcje będą się powtarzać, opcje: przenieść cały batch-OCR w okna, gdy
|
||||
działa tylko jeden worker, albo zrezygnować z fallbacku PIHA (kolejka
|
||||
po prostu czeka na SOLARIĘ).
|
||||
- **Postgres `max_connections`**: każdy proces workera trzyma połączenie.
|
||||
Przy defaultowym 100 i concurrency 4+1 zapasu jest dużo; podbijając
|
||||
`WORKER_CONCURRENCY` pilnować sumy połączeń (formuła w docs upstream).
|
||||
- **Wersje obrazów muszą być identyczne** (PIHA i SOLARIA) — wspólny schemat
|
||||
DB i sygnatury zadań. Podbijać oba compose naraz.
|
||||
|
||||
## NFS: export na PIHA, mount na SOLARIA
|
||||
|
||||
Transfer idzie po **LAN** (PIHA `192.168.31.5` ↔ SOLARIA `192.168.31.70`,
|
||||
1 Gb/s, ten sam switch) — NIE po Tailscale. Przepustowość nie jest wąskim
|
||||
gardłem OCR.
|
||||
|
||||
### Host-side na PIHA (NIE w compose — krok przy deployu modułu 3)
|
||||
|
||||
```
|
||||
# /etc/exports na PIHA — export TYLKO dla SOLARII:
|
||||
/opt/homelab/data/paperless 192.168.31.70(rw,sync,no_subtree_check,no_root_squash)
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo apt install nfs-kernel-server # jesli brak
|
||||
sudo exportfs -ra
|
||||
```
|
||||
|
||||
`no_root_squash` jest potrzebne, bo entrypoint obrazu (root) robi `chown` na
|
||||
katalogach przy starcie kontenera na SOLARII; export jest ograniczony do
|
||||
jednego IP w zaufanym LAN.
|
||||
|
||||
### Strona SOLARII
|
||||
|
||||
Mounty definiuje compose jako named volumes z driverem NFS — **zero wpisów
|
||||
w /etc/fstab**; jedyny host-side wymóg to pakiet klienta:
|
||||
|
||||
```bash
|
||||
sudo apt install nfs-common
|
||||
```
|
||||
|
||||
### UID mapping (krytyczne)
|
||||
|
||||
Pliki na exporcie mają numerycznego właściciela — NFS nie tłumaczy nazw.
|
||||
Dlatego `USERMAP_UID/GID=1000` jest ustawione **w obu** compose (PIHA
|
||||
i SOLARIA); zmiana po jednej stronie = worker traci dostęp do plików.
|
||||
Weryfikacja po deployu: `./healthcheck.sh` robi test zapisu na mount.
|
||||
|
||||
## Cutover checklist (przy deployu modułu 3 — po działającym module 2)
|
||||
|
||||
1. `services/paperless/` działa na PIHA (healthcheck zielony).
|
||||
2. Export NFS na PIHA (wyżej) + `showmount -e 192.168.31.5` z SOLARII.
|
||||
3. `nfs-common` na SOLARII.
|
||||
4. `.env` z `env.example` — sekrety SKOPIOWANE z PIHA, nie nowe.
|
||||
5. `docker compose up -d` + `./healthcheck.sh`.
|
||||
6. Test: wrzucić PDF do consume na PIHA → obciążenie CPU na SOLARII, nie PIHA.
|
||||
7. Test fallbacku: stop workera → zadanie czeka/mieli PIHA → start → drenaż.
|
||||
8. Wpis w `hosts/solaria/services.yaml` + topology (dopiero przy deployu).
|
||||
90
services/paperless-worker/docker-compose.yml
Normal file
90
services/paperless-worker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# 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).
|
||||
command: 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.
|
||||
- PAPERLESS_REDIS=redis://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 DECYZJA OSKARA: sizing pod batch 70k zalacznikow (moduł 5) —
|
||||
# podbic po probce (np. concurrency 8) czy zostawic zapas na ollama/AI?
|
||||
- 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
|
||||
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"
|
||||
16
services/paperless-worker/env.example
Normal file
16
services/paperless-worker/env.example
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# paperless-worker secrets — copy to .env (gitignored) next to
|
||||
# docker-compose.yml on SOLARIA. Values are SHARED with paperless@PIHA:
|
||||
# copy them from /opt/homelab/config or services/paperless/.env on PIHA,
|
||||
# do NOT generate new ones.
|
||||
|
||||
# MUST be identical to PAPERLESS_SECRET_KEY in services/paperless/.env @ PIHA.
|
||||
PAPERLESS_SECRET_KEY=change-me-same-as-piha
|
||||
|
||||
# MUST be identical to PAPERLESS_DBPASS in services/paperless/.env @ PIHA.
|
||||
PAPERLESS_DBPASS=change-me-same-as-piha
|
||||
|
||||
# Parallel OCR tasks on SOLARIA (celery --concurrency). Total CPU load is
|
||||
# roughly WORKER_CONCURRENCY x PAPERLESS_THREADS_PER_WORKER (4x4=16 threads
|
||||
# default, on 24 cores / 32 threads).
|
||||
# TODO DECYZJA OSKARA: sizing pod batch 70k zalacznikow — zmierzyc na probce.
|
||||
WORKER_CONCURRENCY=4
|
||||
28
services/paperless-worker/healthcheck.sh
Executable file
28
services/paperless-worker/healthcheck.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
# Healthcheck for paperless-worker (OCR worker on SOLARIA -> broker @ PIHA)
|
||||
#
|
||||
# NOTE: SOLARIA is session-availability. This script reports the worker's
|
||||
# health while the host is up; the worker being absent because SOLARIA is
|
||||
# off is a normal state handled by the queue on PIHA, not a failure here.
|
||||
|
||||
# Container must be running
|
||||
if ! docker ps --filter "name=paperless-worker" --filter "status=running" | grep -qw "paperless-worker"; then
|
||||
echo "[FAIL] paperless-worker container is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Worker must answer a celery ping via the broker on PIHA — proves both
|
||||
# broker connectivity (192.168.31.5:6380) and a live worker process.
|
||||
if ! docker exec paperless-worker celery --app paperless inspect ping --timeout 10 2>/dev/null | grep -q "pong"; then
|
||||
echo "[FAIL] celery worker does not answer ping (broker @ 192.168.31.5:6380 unreachable or worker dead)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NFS storage from PIHA must be readable AND writable (UID mapping check)
|
||||
if ! docker exec paperless-worker sh -c 'touch /usr/src/paperless/media/.nfs-write-test && rm /usr/src/paperless/media/.nfs-write-test' 2>/dev/null; then
|
||||
echo "[FAIL] NFS media mount from PIHA is not writable (mount down or UID mismatch)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[OK] paperless-worker is healthy"
|
||||
exit 0
|
||||
32
services/paperless-worker/service.yaml
Normal file
32
services/paperless-worker/service.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
service:
|
||||
name: paperless-worker
|
||||
owner_node: solaria
|
||||
role: ocr-compute-worker # heavy OCR for paperless@PIHA; "compute on-demand z fallback" pattern
|
||||
exposure: local-only # no listening ports at all — outbound-only celery worker
|
||||
dependencies:
|
||||
- paperless # broker (redis 192.168.31.5:6380), DB (:5434) and NFS storage @ PIHA
|
||||
ports: [] # none — connects out, serves nothing
|
||||
healthcheck:
|
||||
type: command
|
||||
# celery ping through the broker: proves both broker reachability and a
|
||||
# live worker process. Runs inside the container.
|
||||
endpoint: docker exec paperless-worker celery --app paperless inspect ping --timeout 10
|
||||
interval: 60s
|
||||
timeout: 15s
|
||||
retries: 3
|
||||
restart_policy: unless-stopped
|
||||
# SOLARIA is a session-availability node: this worker being down is a NORMAL
|
||||
# state, not an incident. Queued OCR tasks wait in Redis@PIHA (AOF) and the
|
||||
# built-in concurrency-1 worker on PIHA grinds slowly meanwhile.
|
||||
persistence:
|
||||
# Nothing to back up here — all document state lives on PIHA (owner:
|
||||
# services/paperless). The NFS volumes below are mounts of PIHA data,
|
||||
# backed up on the PIHA side.
|
||||
paths: []
|
||||
runtime:
|
||||
config_files:
|
||||
- .env # secrets shared with paperless@PIHA (gitignored, from env.example)
|
||||
env_vars:
|
||||
- PAPERLESS_SECRET_KEY # MUST equal the value on PIHA
|
||||
- PAPERLESS_DBPASS # MUST equal the value on PIHA
|
||||
- WORKER_CONCURRENCY # optional, default 4 — parallel OCR tasks
|
||||
112
services/paperless/README.md
Normal file
112
services/paperless/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# 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)
|
||||
- `6380` → Redis dla workera@SOLARIA (6379 zajęte przez agent-system-redis)
|
||||
|
||||
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 rozważyć `PAPERLESS_DISABLE_REGULAR_LOGIN=true`
|
||||
(TODO DECYZJA OSKARA 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ń
|
||||
|
||||
### Propozycja backupu (TODO DECYZJA OSKARA — zatwierdzić przed deployem)
|
||||
|
||||
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. Alternatywa/uzupełnienie: offsite (restic → chmura?).
|
||||
VPS odpada (brak miejsca).
|
||||
3. **Retencja (propozycja)**: 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.
|
||||
|
||||
## 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 `paper.kapala.org` → PIHA (DNS Only) + vhost w npm@PIHA (HTTPS → 192.168.31.5:8210).
|
||||
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).
|
||||
139
services/paperless/docker-compose.yml
Normal file
139
services/paperless/docker-compose.yml
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# 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:
|
||||
# TODO DECYZJA OSKARA: potwierdzic domene paper.kapala.org (vs paper.okit.pl)
|
||||
# + wpis DNS (A -> LAN/Tailscale, DNS Only) + vhost w npm@PIHA.
|
||||
- PAPERLESS_URL=https://paper.kapala.org
|
||||
- PAPERLESS_TIME_ZONE=Europe/Warsaw
|
||||
- PAPERLESS_REDIS=redis://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 DECYZJA OSKARA: po zweryfikowaniu logowania OIDC ustawic
|
||||
# PAPERLESS_DISABLE_REGULAR_LOGIN=true + PAPERLESS_REDIRECT_LOGIN_TO_SSO=true?
|
||||
- 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 DECYZJA OSKARA: port 8210 wolny wg inwentaryzacji 2026-06-30 —
|
||||
# potwierdzic na zywym hoscie przed deployem: 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.
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- paperless_redisdata:/data
|
||||
ports:
|
||||
# LAN-only bind for paperless-worker@SOLARIA. 6379 is taken by
|
||||
# agent-system-redis.
|
||||
# TODO DECYZJA OSKARA: Redis bez auth na bindzie LAN (zaufana siec
|
||||
# domowa) — akceptowalne, czy dolozyc requirepass? (wtedy PAPERLESS_REDIS
|
||||
# z haslem przenosi sie do .env po obu stronach)
|
||||
- "${LAN_BIND_IP}:6380:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "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:
|
||||
30
services/paperless/env.example
Normal file
30
services/paperless/env.example
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Paperless secrets + host-local binds — copy to .env (gitignored) next to
|
||||
# docker-compose.yml and fill in real values. Never commit .env.
|
||||
|
||||
# LAN IP of PIHA. All published ports (web 8210, postgres 5434, redis 6380)
|
||||
# bind ONLY to this interface — never 0.0.0.0. The OCR worker on SOLARIA
|
||||
# connects over the same LAN. Verify after host rebuilds: ip -4 addr.
|
||||
LAN_BIND_IP=192.168.31.5
|
||||
|
||||
# Django secret key. Generate once (e.g. openssl rand -base64 48), then never
|
||||
# change — MUST be identical in services/paperless-worker/.env on SOLARIA.
|
||||
PAPERLESS_SECRET_KEY=change-me-long-random-string
|
||||
|
||||
# Postgres password for the paperless DB. Both values MUST be identical
|
||||
# (POSTGRES_PASSWORD initializes the DB, PAPERLESS_DBPASS is how the app
|
||||
# connects). The worker on SOLARIA uses the same value.
|
||||
POSTGRES_PASSWORD=change-me-very-strong
|
||||
PAPERLESS_DBPASS=change-me-very-strong
|
||||
|
||||
# OIDC provider config (django-allauth openid_connect) — single-line JSON.
|
||||
# Lives in .env because it embeds the Forgejo OAuth2 client secret.
|
||||
# Register the app in Forgejo: Settings > Applications >
|
||||
# Redirect URI: https://paper.kapala.org/accounts/oidc/forgejo/login/callback/
|
||||
# then paste client_id + secret below.
|
||||
# TODO DECYZJA OSKARA: potwierdzic domene paper.kapala.org przed rejestracja w Forgejo.
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS={"openid_connect":{"SCOPE":["openid","profile","email"],"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"forgejo","name":"Forgejo","client_id":"CHANGE-ME-CLIENT-ID","secret":"CHANGE-ME-CLIENT-SECRET","settings":{"server_url":"https://forgejo.kapala.org/.well-known/openid-configuration"}}]}}
|
||||
|
||||
# Bootstrap admin (local login) — used until OIDC is verified, then consider
|
||||
# PAPERLESS_DISABLE_REGULAR_LOGIN=true in docker-compose.yml.
|
||||
PAPERLESS_ADMIN_USER=oskar
|
||||
PAPERLESS_ADMIN_PASSWORD=change-me-bootstrap-only
|
||||
34
services/paperless/healthcheck.sh
Executable file
34
services/paperless/healthcheck.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/bin/bash
|
||||
# Healthcheck for Paperless-ngx (web + db + broker) on PIHA
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Ports are bound to the LAN interface only, so localhost won't answer.
|
||||
# Read the bind IP from .env (same file compose uses for the port mapping).
|
||||
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/.env"
|
||||
fi
|
||||
BIND_IP="${LAN_BIND_IP:-127.0.0.1}"
|
||||
|
||||
for c in paperless paperless-db paperless-broker; do
|
||||
if ! docker ps --filter "name=$c" --filter "status=running" | grep -qw "$c"; then
|
||||
echo "[FAIL] $c container is not running"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Web UI must respond (login page redirect counts — follow it)
|
||||
if ! curl -sfL --max-time 10 "http://${BIND_IP}:8210/" > /dev/null; then
|
||||
echo "[FAIL] paperless web is not responding on ${BIND_IP}:8210"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Redis broker must answer on the LAN bind (the SOLARIA worker depends on it)
|
||||
if ! docker exec paperless-broker redis-cli -h "${BIND_IP}" -p 6380 ping | grep -q PONG; then
|
||||
echo "[FAIL] redis broker is not answering on ${BIND_IP}:6380 (worker@SOLARIA cannot connect)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[OK] paperless is healthy"
|
||||
exit 0
|
||||
48
services/paperless/service.yaml
Normal file
48
services/paperless/service.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
service:
|
||||
name: paperless
|
||||
owner_node: piha
|
||||
role: document-management # KB pillar #2 source of truth for scans/invoices (archiwum = REFERENCJA)
|
||||
exposure: private # LAN/Tailscale only via npm@PIHA (paper.kapala.org); NO public ingress
|
||||
dependencies:
|
||||
- forgejo # OIDC identity provider (forgejo.kapala.org)
|
||||
# paperless-worker@SOLARIA is a consumer of this service (Redis broker,
|
||||
# Postgres, NFS-exported storage) — not a dependency; the stack is fully
|
||||
# functional (slow OCR fallback) with SOLARIA powered down.
|
||||
ports:
|
||||
- container: 8000
|
||||
host: 8210 # LAN_BIND_IP only, never 0.0.0.0
|
||||
protocol: tcp
|
||||
- container: 5432
|
||||
host: 5434 # postgres for paperless-worker@SOLARIA (LAN bind)
|
||||
protocol: tcp
|
||||
- container: 6379
|
||||
host: 6380 # redis broker for paperless-worker@SOLARIA (LAN bind)
|
||||
protocol: tcp
|
||||
healthcheck:
|
||||
type: http
|
||||
endpoint: http://192.168.31.5:8210/ # LAN bind — localhost does not answer
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
restart_policy: unless-stopped
|
||||
persistence:
|
||||
# Paperless is the KB REFERENCE archive — this storage is the single source
|
||||
# of truth for documents and its backup is MANDATORY (kb-02 hybrid boundary
|
||||
# condition). data/media/consume are also the NFS export consumed by
|
||||
# paperless-worker@SOLARIA.
|
||||
paths:
|
||||
- /opt/homelab/data/paperless/data # search index, classifier — rebuildable
|
||||
- /opt/homelab/data/paperless/media # ORIGINALS + archived PDFs — irreplaceable
|
||||
- /opt/homelab/data/paperless/consume # ingest drop dir
|
||||
- /opt/homelab/data/paperless/export # document_exporter output (backup staging)
|
||||
- paperless_pgdata # named volume: postgres (metadata, tags, correspondents)
|
||||
- paperless_redisdata # named volume: redis AOF (queued OCR tasks)
|
||||
runtime:
|
||||
config_files:
|
||||
- .env # secrets (gitignored, from env.example)
|
||||
env_vars:
|
||||
- LAN_BIND_IP # required — compose port-bind interpolation (192.168.31.5)
|
||||
- PAPERLESS_SECRET_KEY
|
||||
- PAPERLESS_DBPASS
|
||||
- POSTGRES_PASSWORD # must equal PAPERLESS_DBPASS
|
||||
- PAPERLESS_SOCIALACCOUNT_PROVIDERS # OIDC provider JSON incl. Forgejo client secret
|
||||
Loading…
Reference in a new issue