phase: 01-core-stack
reviewed: 2026-04-17T22:42:00+02:00
depth: standard
files_reviewed: 2
files_reviewed_list:
- docker-compose.yml
- docker-compose.env.template
findings:
critical: 1
warning: 4
medium: 3
low: 2
info: 3
total: 13
status: issues_found
Phase 1: Code Review Report β Core Stack
Reviewed: 2026-04-17T22:42:00+02:00
Depth: standard
Files Reviewed: 2
Status: issues_found
Zusammenfassung
Reviewed: docker-compose.yml (6-service stack) and docker-compose.env.template.
The stack is well-structured. All 6 services have healthchecks, the explicit paperless-net network is in place, Ollama is correctly bound to 127.0.0.1:11434, and no-new-privileges hardening is applied to paperless-ai. The dependency chain (db/broker β webserver β paperless-ai/ollama) is correct.
Three issues require attention before this stack can be trusted in production:
- CRITICAL: The PostgreSQL volume mount path is wrong β data will not persist correctly.
- HIGH:
POSTGRES_PASSWORDis hardcoded in plaintext in the compose file, bypassing the template mechanism entirely. - HIGH: paperless-ai does not have
env_file: docker-compose.env, so all paperless-ai config in the template (API token, API URL, scan interval) is silently ignored at runtime.
CRITICAL
CR-01: PostgreSQL volume mounts to wrong path β data loss on restart
File: docker-compose.yml:47
Issue: The pgdata volume is mounted to /var/lib/postgresql instead of /var/lib/postgresql/data. PostgreSQL 18 stores its data cluster at /var/lib/postgresql/data. Mounting to the parent directory means the actual data directory is a subdirectory of the mount, which can cause the volume to persist an empty parent while data accumulates inside a container-local path. On a container restart after an initial initdb, PostgreSQL may re-initialize (wiping existing data) or fail to find its cluster.
Fix:
db:
volumes:
- pgdata:/var/lib/postgresql/data # war: /var/lib/postgresql
HIGH
HI-01: POSTGRES_PASSWORD ist Klartext in docker-compose.yml β Template-Variable hat keine Wirkung
File: docker-compose.yml:53
Issue: POSTGRES_PASSWORD: paperless is hardcoded inline in the db service environment block. The db service has no env_file directive, so POSTGRES_PASSWORD=CHANGE_ME in docker-compose.env.template (line 51) has no effect. The comment on line 51-53 acknowledges this is deferred to SCRIPT-03/Phase 3, but the current state is a functional plaintext credential. Any operator who copies the template and sets a strong password will be confused when the actual password remains paperless.
Note: the comment in the compose file (lines 51-52) documents this is a known deferred item for Phase 3. This finding confirms the deferral is load-bearing β it must not be forgotten.
Fix (Phase 3): Remove the inline value and add env_file:
db:
env_file: docker-compose.env
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
# POSTGRES_PASSWORD kommt aus docker-compose.env
And in docker-compose.env.template, keep:
POSTGRES_PASSWORD=CHANGE_ME
HI-02: paperless-ai hat kein env_file β API-Token und Scan-Interval werden nie gesetzt
File: docker-compose.yml:89-122 / docker-compose.env.template:20-37
Issue: docker-compose.env.template defines the following variables intended for paperless-ai:
- PAPERLESS_API_TOKEN (line 29)
- PAPERLESS_API_URL (line 30)
- PAPERLESS_URL_AI (line 31)
- SCAN_INTERVAL (line 37)
- ANTHROPIC_API_KEY / OPENROUTER_API_KEY (lines 40-41)
However, the paperless-ai service has no env_file: docker-compose.env directive. These variables are never loaded into the container. At runtime, PAPERLESS_API_TOKEN will be empty/unset, meaning paperless-ai cannot authenticate to the Paperless-ngx API. The service will start (healthcheck passes) but all document scanning will silently fail.
The OLLAMA_API_URL, OLLAMA_MODEL, and AI_PROVIDER are correctly set inline in the environment: block, so the LLM connection works. But the Paperless API connection does not.
Fix: Add env_file to the paperless-ai service and remove the duplicated vars from the inline environment block, or keep inline vars and add only the missing ones:
paperless-ai:
env_file: docker-compose.env
environment:
- PUID=1000
- PGID=1000
- PAPERLESS_AI_PORT=${PAPERLESS_AI_PORT:-3000}
- RAG_SERVICE_URL=http://webserver:8000
- RAG_SERVICE_ENABLED=true
- OLLAMA_API_URL=http://ollama:11434
- OLLAMA_MODEL=llama3.2
- AI_PROVIDER=ollama
# PAPERLESS_API_TOKEN, SCAN_INTERVAL, ANTHROPIC_API_KEY kommen aus docker-compose.env
MEDIUM
ME-01: paperless-ai Healthcheck-Port ist hardcoded β bricht wenn PAPERLESS_AI_PORT geaendert wird
File: docker-compose.yml:118
Issue: The healthcheck tests http://localhost:3000 hardcoded, but the service port is controlled by ${PAPERLESS_AI_PORT:-3000}. If an operator sets PAPERLESS_AI_PORT=3500 in docker-compose.env, the healthcheck will always fail (testing the wrong port) while the service runs fine on 3500. This would block dependent services from ever reaching service_healthy.
Fix:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:${PAPERLESS_AI_PORT:-3000}"]
Or simpler: hardcode 3000 in ports AND healthcheck and remove the variable indirection, since it has no documented use case for being changed.
ME-02: PAPERLESS_AI_PORT fehlt im Template
File: docker-compose.env.template
Issue: docker-compose.yml references ${PAPERLESS_AI_PORT:-3000} (lines 107, 114) but PAPERLESS_AI_PORT does not appear in docker-compose.env.template. An operator reading the template to understand all configurable variables will not know this variable exists.
Fix: Add to template under the === paperless-ai === section:
# Port fuer paperless-ai Web-UI (Standard: 3000)
PAPERLESS_AI_PORT=3000
ME-03: Gitea SSH-Port 222 ist ohne localhost-Binding exponiert
File: docker-compose.yml:160
Issue: Gitea exposes SSH on "222:22" without binding to 127.0.0.1. This means the SSH port is accessible on all host network interfaces, including any externally reachable ones. Given OhnePapier's privacy-first, local-only threat model, this mirrors the Ollama threat (T-01-01) but was not hardened.
Fix:
ports:
- "127.0.0.1:3001:3000"
- "127.0.0.1:222:22"
LOW
LO-01: Gitea fehlen Sicherheits-Haertungen die paperless-ai hat
File: docker-compose.yml:148-170
Issue: paperless-ai has cap_drop: [ALL] and security_opt: [no-new-privileges=true]. Gitea has neither. For consistency with the project's hardening posture, Gitea should receive the same treatment β it runs as a known user (USER_UID=1000) and does not need extra capabilities.
Fix:
gitea:
cap_drop:
- ALL
security_opt:
- no-new-privileges=true
LO-02: Alle drei neuen Dienste nutzen floating :latest Tags
File: docker-compose.yml:62, 125, 149
Issue: paperless-ngx:latest, ollama/ollama:latest, and gitea/gitea:latest use floating tags. This means docker compose pull can silently pull a breaking change. For a local, self-hosted stack this is a higher risk than in a CI pipeline β there is no rollback mechanism if a pulled image breaks document processing.
This is a LOW rather than MEDIUM because the project is v0.0 and pinning can be deferred β but it should happen before the stack processes real documents in production.
Fix: Pin to a specific release after verifying compatibility:
# paperless-ngx:2.14.7
# ollama/ollama:0.6.5
# gitea/gitea:1.23.5
INFO
IN-01: paperless-ai environment-Block enthaelt Variablen, die auch im Template stehen β doppelte Quelle der Wahrheit
File: docker-compose.yml:111 / docker-compose.env.template:35-36
Issue: OLLAMA_API_URL, OLLAMA_MODEL, and AI_PROVIDER are defined both inline in the compose file environment block and in the template. Once env_file is added to paperless-ai (see HI-02), inline values take precedence over env_file values in Docker Compose. This is fine for non-secret operational config but creates confusion about where the authoritative value lives.
Suggestion: Keep non-secret operational defaults inline (OLLAMA_API_URL, OLLAMA_MODEL, AI_PROVIDER) and keep only secrets and user-specific values in the template. Add a comment to both files to document the split.
IN-02: Keine expliziten container_name fuer broker, db, webserver
File: docker-compose.yml:27-87
Issue: ollama and gitea have explicit container_name declarations, but broker, db, and webserver do not β they get Docker-generated names (paperless-broker-1 etc., based on COMPOSE_PROJECT_NAME). The CLAUDE.md command reference uses paperless-webserver-1 explicitly. If COMPOSE_PROJECT_NAME is not set to paperless in .env, these names will differ and the documented commands will break.
Suggestion: Either add explicit container_name to all services, or ensure .env contains COMPOSE_PROJECT_NAME=paperless and document the dependency.
IN-03: Ollama-Healthcheck prueft nur Erreichbarkeit, nicht ob ein Modell geladen ist
File: docker-compose.yml:142-146
Issue: The Ollama healthcheck (curl -f http://localhost:11434) verifies the API server is responding but not that llama3.2 is available. On a fresh install, docker compose up will succeed and paperless-ai will start and try to call Ollama, but model inference will fail until ollama pull llama3.2 has been run manually.
This is expected behavior for this phase (model pulling is likely a separate setup step), but it should be documented in the pre-flight checklist in CLAUDE.md.
Suggestion: Add to the Fresh Install Pre-flight section in CLAUDE.md:
4. **Ollama-Modell vorhanden** β `ollama pull llama3.2` muss vor dem ersten paperless-ai-Scan ausgefuehrt worden sein.
Zusammenfassung der Befunde
| ID | Schwere | Datei | Kurzfassung |
|---|---|---|---|
| CR-01 | CRITICAL | docker-compose.yml:47 | pgdata-Volume-Pfad fehlt /data β Datenverlust-Risiko |
| HI-01 | HIGH | docker-compose.yml:53 | POSTGRES_PASSWORD Klartext inline β Template hat keine Wirkung |
| HI-02 | HIGH | docker-compose.yml:89 | paperless-ai ohne env_file β API-Token wird nie gesetzt |
| ME-01 | MEDIUM | docker-compose.yml:118 | Healthcheck-Port hardcoded β bricht bei PAPERLESS_AI_PORT-Aenderung |
| ME-02 | MEDIUM | docker-compose.env.template | PAPERLESS_AI_PORT fehlt im Template |
| ME-03 | MEDIUM | docker-compose.yml:160 | Gitea SSH-Port ohne localhost-Binding |
| LO-01 | LOW | docker-compose.yml:148 | Gitea ohne cap_drop / no-new-privileges |
| LO-02 | LOW | docker-compose.yml:62,125,149 | Drei Dienste nutzen floating :latest Tags |
| IN-01 | INFO | docker-compose.yml:111 | Doppelte Quelle der Wahrheit fuer Ollama-Variablen |
| IN-02 | INFO | docker-compose.yml:27 | Fehlende container_name β COMPOSE_PROJECT_NAME-Abhaengigkeit undokumentiert |
| IN-03 | INFO | docker-compose.yml:142 | Ollama-Healthcheck prueft kein Modell β Preflight unvollstaendig |
Reviewed: 2026-04-17T22:42:00+02:00
Reviewer: Claude (gsd-code-reviewer)
Depth: standard