# Docker's default json-file driver keeps container logs forever; on a long-running # box that fills the disk. Rotated at 10 MB, three files per container. x-logging: &default-logging driver: json-file options: max-size: "10m" max-file: "3" services: qdrant: # Pinned: server-side hybrid (prefetch + RRF fusion) depends on the Qdrant version, # and :latest silently changed it under an existing install on every pull. image: ${QDRANT_IMAGE:-qdrant/qdrant:v1.19.1} restart: unless-stopped logging: *default-logging security_opt: - no-new-privileges:true # Loopback only: the API reaches Qdrant over the compose network, and these ports # exist for running the API directly on the host (`uv run uvicorn ...`). Published # on 0.0.0.0 they let anyone on the LAN read or drop the collection, with no key — # around API_KEY entirely. ports: - "127.0.0.1:${QDRANT_PORT:-6333}:6333" - "127.0.0.1:${QDRANT_GRPC_PORT:-6334}:6334" environment: # Qdrant reports anonymous usage statistics unless told not to; DocRAG makes no # calls home (bundled fonts, LiteLLM's local cost map), so neither does its store. QDRANT__TELEMETRY_DISABLED: "true" # To require a key (set QDRANT_API_KEY in .env too, so the API sends it): # QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY} volumes: - qdrant_storage:/qdrant/storage healthcheck: # /readyz (open even with an API key) answers 200 once collections are loaded; an # open TCP port only proved the process had started. test: - CMD-SHELL - >- timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/6333 && printf "GET /readyz HTTP/1.0\r\n\r\n" >&3 && read -r _ status _ <&3 && [ "$$status" = 200 ]' interval: 10s timeout: 6s retries: 12 api: # The released image (ghcr.io, see .github/workflows/release.yml) when pulled, the one # built here otherwise: DOCRAG_PULL_POLICY=always with DOCRAG_VERSION= runs it without building. The default builds from source, as before. A fork # publishing its own images sets DOCRAG_IMAGE (ghcr.io//). image: ${DOCRAG_IMAGE:-ghcr.io/jaswanth1524/retrieval-ai-platform}:${DOCRAG_VERSION:-local} pull_policy: ${DOCRAG_PULL_POLICY:-build} build: context: . dockerfile: Dockerfile args: # Optional uv extras baked into the image, e.g. UV_EXTRAS=ocr for scanned PDFs. # Build-time only: a pulled release (DOCRAG_PULL_POLICY=always) has no OCR, and # this setting does nothing there — build instead to get it. UV_EXTRAS: ${UV_EXTRAS:-} restart: unless-stopped # Running ingests finish when the container is stopped (queued ones are failed), and # a large PDF can take minutes: Docker's default 10 s killed them midway. stop_grace_period: 60s logging: *default-logging # Hardened: the image is immutable at runtime. Writable places are only the model # cache volume, the /app/data volume, and a tmpfs /tmp (upload # spooling). No Linux capabilities and no privilege gain: the process needs neither. # tmpfs is RAM, counted against mem_limit: a large POST /import upload spools there, # so raise API_MEM_LIMIT by the backup's size to restore a big one. (/export streams # its zip as it writes it and needs no room here.) read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true # The reranker peaks around 1.4 GB with the default int8 model and 2.3-2.7 GB with fp32 # (RERANKER_ONNX_FILE=; README "Latency"). A cap keeps a runaway request from taking # the whole Docker VM down with it; restart brings it back. mem_limit: ${API_MEM_LIMIT:-4g} env_file: - .env.example # Optional local override — copy .env.example to .env and set real secrets # (e.g. OPENAI_API_KEY) there; values here win over .env.example. - path: .env required: false environment: QDRANT_URL: http://qdrant:6333 # Qdrant's own port inside the compose network, whatever QDRANT_GRPC_PORT maps # on the host. QDRANT_GRPC_PORT: "6334" # Cold-start the embedding/reranker models at boot rather than on the first # real request — worth the extra startup time in a long-running container. WARMUP_MODELS: "true" # Same Docker network as Qdrant, so gRPC is reachable without extra config. QDRANT_PREFER_GRPC: "true" # Pinned to the model_cache volume: a host path set in .env for running the API # outside Docker would otherwise land on the read-only root filesystem, and every # model download would fail. FASTEMBED_CACHE_PATH: /home/app/.cache/fastembed HF_HOME: /home/app/.cache/huggingface # /app/data holds whatever is turned on in .env: the sqlite job store # (JOB_STORE_BACKEND=sqlite), kept originals (RAW_DOCUMENT_DIR=./data/originals, # which also enables re-index and a complete GET /export) and ratings # (FEEDBACK_ENABLED=true). All three are off by default, and their default paths are # under /app/data, so the docrag_data volume below covers them. The Dockerfile # creates /app/data owned by uid 1000, which is what makes the volume writable by the # non-root process — without that, Docker seeds it root-owned. A store that is on but # can't write its directory stops startup with a message naming the setting. volumes: # Downloaded embedding/reranker models (~0.5 GB; ~1.3 GB with the fp32 reranker, # RERANKER_ONNX_FILE=). Without this every container # recreate — including each `docker compose up --build` — downloads them again. - model_cache:/home/app/.cache - docrag_data:/app/data # Keep the index in step with a host folder: mount it read-only and set # WATCH_DIR=/app/watched in .env (README "Watched folder"). # - ./my-documents:/app/watched:ro extra_hosts: # Lets OLLAMA_BASE_URL=http://host.docker.internal:... resolve on native Linux # Docker (Docker Desktop on macOS/Windows already resolves this automatically). - "host.docker.internal:host-gateway" # Loopback by default, like Qdrant above: with API_KEY empty (the default) anyone who # can reach this port can upload, delete and download the whole corpus. Set # API_BIND=0.0.0.0 in .env to serve other devices on the network, and set API_KEY. ports: - "${API_BIND:-127.0.0.1}:${API_PORT:-8000}:8000" healthcheck: # Liveness only (probes /health, not /health/ready) — deliberately doesn't # depend on Ollama/Qdrant reachability, so a slow/unreachable Ollama can't # flap the container's own health status. test: ["CMD", "/app/.venv/bin/python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=4).status == 200 else 1)"] interval: 30s timeout: 5s # First boot downloads the models (~0.5 GB) before /health answers; on a slow # link 120s marked a healthy container unhealthy. start_interval probes often # during that window, so a fast boot still reports healthy quickly. start_period: 600s start_interval: 5s retries: 3 depends_on: qdrant: condition: service_healthy volumes: qdrant_storage: model_cache: # The api service's /app/data (job store, kept originals, feedback). docrag_data: