name: recensa services: recensa: build: context: . dockerfile: Dockerfile image: recensa:latest container_name: recensa # Publish to the host loopback only: the Host/Origin allowlist is DNS-rebinding defense-in-depth, not access control, # and is useless against non-browser direct connections that can forge the Host header -> binding to 127.0.0.1 returns access control to the network layer (single-machine personal use). # WARNING: exposing on the LAN (0.0.0.0) gives anyone on your network full read + irreversible delete access to every transcript. # The Host/Origin allowlist is DNS-rebinding defense, NOT access control (a non-browser client such as curl forges Host: localhost and walks past it). # For LAN/remote access: change to "0.0.0.0:7788:7788", add your own auth/reverse-proxy FIRST, and set RECENSA_ALLOWED_HOSTS. ports: # The host port can be overridden via RECENSA_HOST_PORT (default 7788) -> change it to coexist with an existing local service on 7788; inside the container it is always 7788. - "127.0.0.1:${RECENSA_HOST_PORT:-7788}:7788" environment: SESSION_VIEWER_HOST: "0.0.0.0" # RECENSA_PROJECTS_DIR is baked into the image (/data/claude/projects) — no need to repeat it here. # bind-mount fs-events are unreliable (Docker Desktop Win/Mac disables inotify) -> the watcher auto-detects /.dockerenv and uses tick mode: # adaptive hot-spot incremental scan (active session hot-scan ~700ms, idle cold full-scan 5 min, all in the index worker -> no main-thread freeze, far less CPU than continuous polling). # Overridable: RECENSA_WATCH_MODE=events|tick, RECENSA_TICK_HOT_MS, RECENSA_FULL_SCAN_MS # Linux only: the container starts as root and docker-entrypoint.sh re-homes # its internal appuser to match the bind-mounted directory's actual uid/gid # before dropping privileges, so it can read the host's ~/.claude/projects # (created 700/600, owner-only) with no configuration. Set PUID/PGID here only # to override that detection (e.g. the mount's owner is not the intended uid). PUID: "${PUID:-}" PGID: "${PGID:-}" volumes: # original session JSONL — writable so delete-session can physically remove files (parity with native host). # App discipline keeps it read-only in practice: only deleteSession writes/removes; indexer/parser/reconstruct never write here. # bind.selinux: z relabels the mount so SELinux-enforcing hosts (RHEL/Fedora/CentOS, # Podman) don't deny access even when the uid/gid already match; ignored outright # on hosts without SELinux, so this is always safe to set. - type: bind source: "${CLAUDE_HOME:-${HOME}/.claude}" target: /data/claude bind: selinux: z # DB persistence: viewer.db (rebuildable cache) + flags.db (precious user data — pins/tags/notes) - recensa-data:/app/backend/data restart: unless-stopped init: true # PID 1 signal forwarding / zombie reaping (running node directly also works, but init is more robust) # healthcheck comes from the image (see Dockerfile HEALTHCHECK) so every deploy path # gets the same probe — docker run, prebuilt compose, and this file alike. volumes: recensa-data: