services: obsidian-sync: build: . container_name: obsidian-headless-sync restart: unless-stopped user: "${UID}:${GID}" environment: - HOME=/home/node volumes: - ./config:/home/node/.config - ./vault:/home/node/vault # `ob sync --continuous` has no HTTP port and this slim image has no # ps/pgrep, so there's nothing to curl or pgrep for like obsidian-mcp's # healthcheck. Instead: tee its own log output to a file and check that # file's mtime — it logs continuously (a "Fully synced" heartbeat every # ~30s when idle, plus real activity while syncing), so a file that # hasn't been touched in minutes means the process is hung/stuck, not # just between syncs. The overridden entrypoint replaces the Dockerfile's # exec-form CMD with a shell so the pipe to `tee` works. entrypoint: ["sh", "-c", "ob sync --continuous 2>&1 | tee /tmp/sync-heartbeat.log"] healthcheck: test: ["CMD-SHELL", "find /tmp/sync-heartbeat.log -mmin -3 | grep -q ."] interval: 60s timeout: 5s retries: 3 start_period: 30s obsidian-mcp: build: . container_name: obsidian-mcp-server restart: unless-stopped user: "${UID}:${GID}" # --stateful + --sessionTimeout: without these, supergateway's default # stateless stdio->StreamableHttp mode spawns a fresh `mcpvault` child # process per request and never reaps it (no session object exists to # attach a timeout to). Over days that leaked hundreds of MB of RAM into # orphaned node processes and eventually forced a full Pi reboot. # --stateful reuses one child per session; --sessionTimeout kills an # idle session (and its child process) after 10 minutes of inactivity. entrypoint: [ "supergateway", "--port", "3000", "--outputTransport", "streamableHttp", "--stateful", "--sessionTimeout", "600000", "--stdio", "npx -y @bitbonsai/mcpvault@latest /data/vault" ] environment: - OBSIDIAN_API_KEY=${OBSIDIAN_API_KEY} # UID:GID below has no /etc/passwd entry in this image, so HOME # defaults to "/" and npm's `npx -y @bitbonsai/mcpvault` (spawned # per session by supergateway) fails EACCES trying to mkdir # /.npm on the root-owned filesystem — every session's child dies # before it can answer `initialize`, which reads from the MCP # client as a connection timeout. /tmp is world-writable (1777) # regardless of UID, so pointing HOME there gives npm somewhere # it can actually create a cache. - HOME=/tmp volumes: - ./vault:/data/vault ports: - "3000:3000" # Defense in depth: even if a future leak slips past --stateful, cap # this container's memory so Docker restarts it long before it can # swap the whole Pi into a stall. mem_limit: 768m # Process/port liveness only — any HTTP response (even a 404) proves # supergateway is up and listening, so plain curl without -f is # intentional (curl exits non-zero only on a failed connection, not on # an HTTP error status). This does NOT detect an individual expired # MCP session (that's expected, client-side behaviour — Jarvis's own # MCP client now reconnects on a stale session instead) — it only # catches the container itself being hung, crashed, or unreachable. healthcheck: test: ["CMD", "curl", "-s", "-o", "/dev/null", "http://localhost:3000/"] interval: 30s timeout: 5s retries: 3 start_period: 10s