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:

  1. CRITICAL: The PostgreSQL volume mount path is wrong β€” data will not persist correctly.
  2. HIGH: POSTGRES_PASSWORD is hardcoded in plaintext in the compose file, bypassing the template mechanism entirely.
  3. 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