services: postgres: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:?Set POSTGRES_DB in .env (see .env.example)} POSTGRES_USER: ${POSTGRES_USER:?Set POSTGRES_USER in .env (see .env.example)} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env (see .env.example)} TZ: ${TZ} volumes: - ${DISCVAULT_NEXT_POSTGRES_DATA:-./postgres-data}:/var/lib/postgresql/data healthcheck: # Resolved inside the container, not by Compose on the host, so a missing # host-side variable cannot turn this into an invalid command that fails # forever. Probing TCP instead of the Unix socket keeps the check false # during initdb, when a temporary socket-only server is briefly running. test: - CMD-SHELL - 'pg_isready -h 127.0.0.1 -p 5432 -U "$${POSTGRES_USER:-postgres}" -d "$${POSTGRES_DB:-postgres}"' interval: 10s timeout: 5s retries: 10 # First start runs initdb and can be slow on NAS storage. Without this, # early failures mark the container unhealthy and abort the whole `up`. start_period: 120s networks: - discvault-next next-api: image: ${DISCVAULT_NEXT_IMAGE} restart: unless-stopped working_dir: /opt/discvault/backend command: [ "sh", "-c", "python next_database.py migrate && exec gunicorn --bind 0.0.0.0:5000 --workers ${DISCVAULT_NEXT_API_WORKERS} --timeout ${DISCVAULT_NEXT_API_TIMEOUT} next_app:app" ] environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} TZ: ${TZ} BUILD_VERSION: ${BUILD_VERSION} DISCVAULT_BACKEND_VERSION: ${BUILD_VERSION} DISCVAULT_LEGACY_DATA_DIR: /data JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env; generate it with openssl rand -base64 48} DISCVAULT_NEXT_KEY_ENCRYPTION_KEY: ${DISCVAULT_NEXT_KEY_ENCRYPTION_KEY:-} RP_ID: ${RP_ID} RP_NAME: ${RP_NAME} RP_ORIGINS: ${RP_ORIGINS} LEGACY_AUTH_ENABLED: ${LEGACY_AUTH_ENABLED:-false} # Only next-api terminates HTTP, so only next-api needs to know which hop # may speak for a client. Empty means no forwarding header is believed. DISCVAULT_TRUSTED_PROXIES: ${DISCVAULT_TRUSTED_PROXIES:-} # Only next-api serves pages, so only next-api can be crawled. Default on: # known search-engine and AI-crawler user agents get a 403, every response # carries X-Robots-Tag: noindex, and /robots.txt disallows everything. DISCVAULT_BLOCK_CRAWLERS: ${DISCVAULT_BLOCK_CRAWLERS:-true} DISCVAULT_NEXT_ENABLE_TEST_RESET: ${DISCVAULT_NEXT_ENABLE_TEST_RESET:-false} DISCVAULT_ADMIN_DEDUP_EXECUTE_ENABLED: ${DISCVAULT_ADMIN_DEDUP_EXECUTE_ENABLED:-false} DISCVAULT_NEXT_API_STATEMENT_TIMEOUT_MS: ${DISCVAULT_NEXT_API_STATEMENT_TIMEOUT_MS:-10000} DISCVAULT_NEXT_API_LOCK_TIMEOUT_MS: ${DISCVAULT_NEXT_API_LOCK_TIMEOUT_MS:-5000} # /mcp on the API port is a proxy to the MCP server. Here that server is # its own container, so the code default (127.0.0.1:6090, right for the # all-in-one image) is the API's own loopback and always refuses. This is # the container address: DISCVAULT_NEXT_MCP_PORT republishes the host # port and leaves 6090 inside. DISCVAULT_MCP_URL: ${DISCVAULT_MCP_URL:-http://next-mcp:6090} volumes: - ${DISCVAULT_DATA_DIR}:/data ports: - "${DISCVAULT_NEXT_API_PORT}:5000" depends_on: postgres: # Ordering only. Gating on service_healthy makes a slow or recovering # database abort the whole `up`; next_database.wait_for_database() # blocks here instead, so the stack converges on its own. condition: service_started healthcheck: test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/next/health')" ] interval: 10s timeout: 5s retries: 5 # Runs pending migrations before Gunicorn binds. A first start on a fresh # database applies every migration, so keep this generous. start_period: 120s networks: - discvault-next next-worker: image: ${DISCVAULT_NEXT_IMAGE} restart: unless-stopped working_dir: /opt/discvault/backend command: ["python", "next_worker.py", "work"] environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} TZ: ${TZ} DISCVAULT_WORKER_ID: ${DISCVAULT_WORKER_ID} DISCVAULT_WORKER_POLL_INTERVAL: ${DISCVAULT_WORKER_POLL_INTERVAL} DISCVAULT_LEGACY_DATA_DIR: /data JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in .env; generate it with openssl rand -base64 48} DISCVAULT_NEXT_KEY_ENCRYPTION_KEY: ${DISCVAULT_NEXT_KEY_ENCRYPTION_KEY:-} volumes: - ${DISCVAULT_DATA_DIR}:/data depends_on: next-api: # Ordering only. The worker waits for PostgreSQL and for the # background_jobs table itself, so it does not need next-api to be # *healthy* (migrated) before it starts. condition: service_started healthcheck: disable: true networks: - discvault-next next-mcp: image: ${DISCVAULT_NEXT_IMAGE} restart: unless-stopped working_dir: /opt/discvault/mcp-server command: [ "python", "server.py", "--http", "--host", "0.0.0.0", "--port", "6090" ] environment: DISCVAULT_API: http://next-api:5000 DISCVAULT_MCP_VERSION: ${BUILD_VERSION} TZ: ${TZ} ports: - "${DISCVAULT_NEXT_MCP_PORT:-6090}:6090" depends_on: next-api: # next-mcp calls the API per request, not at boot, and retries via its # own restart policy. condition: service_started healthcheck: test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:6090/health')" ] interval: 10s timeout: 5s retries: 5 start_period: 10s networks: - discvault-next import-sqlite: image: ${DISCVAULT_NEXT_IMAGE} profiles: ["tools"] restart: "no" working_dir: /opt/discvault/backend command: [ "python", "next_import.py", "--db", "/data/discvault.db", "--data-dir", "/data" ] environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} TZ: ${TZ} volumes: - ${DISCVAULT_DATA_DIR}:/data:ro depends_on: postgres: condition: service_healthy networks: - discvault-next networks: discvault-next: name: ${DISCVAULT_NEXT_NETWORK_NAME:-discvault-next}