cognee Documentation

Cognee is the open-source AI memory platform for agents. Give your AI agents persistent long-term memory with small models for free

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy cognee on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name cognee -p 8000:8000 --restart always neo4j:5.26
Configured Services & Ports
โš™๏ธ cognee
image: build locally
๐Ÿ”Œ Ports: 8000:8000, 5678:5678
โš™๏ธ cognee-mcp
image: build locally
๐Ÿ”Œ Ports: 8001:8000, 5679:5678
โš™๏ธ frontend
image: cognee/cognee-ui:${COGNEE_UI_TAG:-latest}
๐Ÿ”Œ Ports: 3000:3000
โš™๏ธ frontend-dev
image: build locally
๐Ÿ”Œ Ports: 3000:3000
โš™๏ธ neo4j
image: neo4j:5.26
๐Ÿ”Œ Ports: 7474:7474, 7687:7687
โš™๏ธ postgres
image: pgvector/pgvector:pg17
๐Ÿ”Œ Ports: 5432:5432
โš™๏ธ redis
image: redis:7-alpine
๐Ÿ”Œ Ports: 6379:6379
โš™๏ธ redisinsight
image: redislabs/redisinsight:latest
๐Ÿ”Œ Ports: 5540:5540
docker-compose.yml
yaml (docker-compose.yml)
services:
  cognee:
    container_name: cognee
    restart: always
    # Must exceed the shutdown hook's background-task drain (default 8s,
    # BACKGROUND_DRAIN_TIMEOUT_SECONDS) plus the engine close/WAL checkpoint โ€”
    # raise BOTH together, or the worker is SIGKILLed mid-checkpoint.
    stop_grace_period: 15s
    networks:
      - cognee-network
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      # Source bind mount is for dev reload only; persistence lives in the
      # named volumes below (shared with cognee-mcp, both run as uid 1000).
      - ./cognee:/app/cognee
      - .env:/app/.env:ro
      - cognee_system:/cognee-storage/system
      - cognee_data:/cognee-storage/data
      # Uncomment to allow ingestion of local files from host:
      # - /path/to/your/data:/data
    environment:
      - DEBUG=false # Change to true if debugging
      - ENV=local
      - LOG_LEVEL=INFO
      - SYSTEM_ROOT_DIRECTORY=/cognee-storage/system
      - DATA_ROOT_DIRECTORY=/cognee-storage/data
      # Relational DB configuration โ€” defaults to embedded SQLite. For
      # `--profile postgres` set DB_PROVIDER=postgres and DB_HOST=postgres
      # (the service name) in your environment or .env.
      - DB_PROVIDER=${DB_PROVIDER:-sqlite}
      - DB_HOST=${DB_HOST:-host.docker.internal}
      - DB_PORT=${DB_PORT:-5432}
      - DB_NAME=${DB_NAME:-cognee_db}
      - DB_USERNAME=${DB_USERNAME:-}
      - DB_PASSWORD=${DB_PASSWORD:-}
      # CAUTION: Default '*' allows all origins. Override with specific domains in production.
      - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}
      # Password login for the default user. This stack is local, so the
      # well-known dev password is the default: `docker compose up` and the UI
      # at localhost:3000 work with no configuration, the same way the
      # POSTGRES_PASSWORD line in any database image's compose does. Override
      # it here or in the environment. The server sets it once on a default
      # user that has no password yet; it never changes an existing password.
      # Do NOT reuse this value on a server reachable from a network -- there,
      # leave it unset and no default-user login exists.
      - DEFAULT_USER_PASSWORD=${DEFAULT_USER_PASSWORD:-default_password}
    extra_hosts:
      # Allows the container to reach your local machine using "host.docker.internal" instead of "localhost"
      - "host.docker.internal:host-gateway"
    ports:
      - 8000:8000
      - 5678:5678 # Debugger port
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: "4.0"
          memory: 8GB

  # Cognee MCP Server - Model Context Protocol server for IDE integration
  cognee-mcp:
    container_name: cognee-mcp
    profiles:
      - mcp
    networks:
      - cognee-network
    build:
      context: .
      dockerfile: cognee-mcp/Dockerfile
    command: --no-migration
    volumes:
      - .env:/app/.env:ro
      - ./cognee:/app/cognee
      # Same storage volumes as the main service: shared memory between the
      # API and the MCP server is the point of running both (same uid 1000).
      - cognee_system:/cognee-storage/system
      - cognee_data:/cognee-storage/data
      # Uncomment to allow ingestion of local files from host:
      # - /path/to/your/data:/data
    environment:
      - DEBUG=false # Change to true if debugging
      - ENV=local
      - LOG_LEVEL=INFO
      - SYSTEM_ROOT_DIRECTORY=/cognee-storage/system
      - DATA_ROOT_DIRECTORY=/cognee-storage/data
      # Streamable HTTP rather than the legacy sse transport. Endpoint: /mcp.
      # The container binds 0.0.0.0, so the Host/Origin (DNS-rebinding) guard
      # rejects requests arriving by LAN IP or a custom hostname with HTTP 421
      # by design; allow them explicitly with
      # MCP_ALLOWED_HOSTS=192.168.1.50:*,myhost.local:* (the ":*" port glob is
      # required), or MCP_DISABLE_DNS_REBINDING_PROTECTION=true to turn it off.
      - TRANSPORT_MODE=http
      # Database configuration - should match the main cognee service
      - DB_PROVIDER=${DB_PROVIDER:-sqlite}
      - DB_HOST=${DB_HOST:-host.docker.internal}
      - DB_PORT=${DB_PORT:-5432}
      - DB_NAME=${DB_NAME:-cognee_db}
      - DB_USERNAME=${DB_USERNAME:-}
      - DB_PASSWORD=${DB_PASSWORD:-}
      # MCP specific configuration
      - PYTHONUNBUFFERED=1
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      # Host ports differ from the main `cognee` service so both can run together
      # (the MCP server still listens on 8000 inside the container).
      - "8001:8000" # MCP port
      - "5679:5678" # MCP debugger port
    healthcheck:
      # The MCP runtime image is python-slim (no curl), so probe with urllib.
      test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health', timeout=5).status==200 else 1)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 4GB

  # cognee UI. `--profile ui` runs the published image, so a user without the
  # repo gets the same thing CI publishes; `--profile ui-dev` builds the
  # hot-reloading stage from source for frontend work.
  #
  # NOTE: the UI is a work in progress and covers the minimum set of features
  # needed to be functional. For a richer experience you can also integrate the
  # cognee MCP server into Cursor / Claude Desktop / VS Code (through Cline/Roo).
  frontend:
    container_name: frontend
    restart: always
    image: cognee/cognee-ui:${COGNEE_UI_TAG:-latest}
    profiles:
      - ui
    environment:
      # Empty means "derive it from the address the browser used", which is
      # correct for the default localhost setup. The browser talks to the
      # backend directly, so when you do set this it must be the address as
      # seen from the browser, not the `cognee` service name.
      - COGNEE_BACKEND_URL=${COGNEE_BACKEND_URL:-}
      # Same value as the backend's: the UI's server-side routes fall back to a
      # default-user login when the browser sent no credentials.
      - DEFAULT_USER_PASSWORD
    depends_on:
      cognee:
        condition: service_healthy
    ports:
      - 3000:3000
    networks:
      - cognee-network

  frontend-dev:
    container_name: frontend-dev
    restart: always
    profiles:
      - ui-dev
    build:
      context: ./cognee-frontend
      dockerfile: Dockerfile
      target: dev
    environment:
      - COGNEE_BACKEND_URL=${COGNEE_BACKEND_URL:-}
      - DEFAULT_USER_PASSWORD
    volumes:
      - ./cognee-frontend/src:/app/src
      - ./cognee-frontend/public:/app/public
    ports:
      - 3000:3000
      # - 9229:9229 # Debugging
    networks:
      - cognee-network

  neo4j:
    image: neo4j:5.26  # Pinned to 5.x; compatible with neo4j Python driver >=5.28,<6
    container_name: neo4j
    restart: always
    profiles:
      - neo4j
    ports:
      - 7474:7474
      - 7687:7687
    environment:
      - NEO4J_AUTH=neo4j/pleaseletmein
      - NEO4J_PLUGINS=["apoc", "graph-data-science"]
    networks:
      - cognee-network

  postgres:
    image: pgvector/pgvector:pg17
    container_name: postgres
    restart: always
    profiles:
      - postgres
    environment:
      POSTGRES_USER: cognee
      POSTGRES_PASSWORD: cognee
      POSTGRES_DB: cognee_db
    # Persist data on a named volume so it survives container recreate. The
    # docker-compose e2e (test_postgres_persistence_across_recreate) depends on
    # this โ€” without it, a recreated postgres container starts empty.
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    networks:
      - cognee-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U cognee -d cognee_db"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

  redis:
    image: redis:7-alpine
    container_name: redis
    profiles:
      - redis
    ports:
      - "6379:6379"
    networks:
      - cognee-network
    volumes:
      - redis_data:/data
    command: [ "redis-server", "--appendonly", "yes" ]


  redisinsight:
    image: redislabs/redisinsight:latest
    container_name: redisinsight
    restart: always
    ports:
      - "5540:5540"
    networks:
      - cognee-network


networks:
  cognee-network:
    name: cognee-network

volumes:
  cognee_system:
  cognee_data:
  postgres_data:
  redis_data:

Technical Documentation: topoteretes/cognee

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (README.md)

๐Ÿ“„ Read the research paper: Optimizing the Interface Between Knowledge Graphs and LLMs for Complex Reasoning โ€” Markovic et al., 2025

When to use Cognee

  • Build a Company Brain. Bring documentation, conversations, tickets, code, and agent work into shared memory. Help your team and agents connect a decision to the discussion and implementation behind it. Explore Company Brain.
  • Give agents memory across runs. Retain project context, past decisions, fixes, and learned rules. Distill useful session lessons into durable knowledge that another session can retrieve. Connect your agent.
  • Ground agents in your domain. Structure memory around the entities and relationships your application needs, with custom data models and ontologies. Explore ontologies.

Choose your starting point

I want toโ€ฆ Start here
Build memory without an LLM Local Python quickstart
Explore a prebuilt graph without downloading models Bundled demo
Generate answers with a local or hosted LLM Optional LLM setup
Give an existing agent memory Plugins and MCP
Run Cognee on my infrastructure Deployment options
Use a managed service Cognee Cloud

Quickstart

Requires Python 3.10โ€“3.14.

1. Install Cognee with pip, uv, or your preferred Python package manager. The gliner extra brings the local extraction model used when no LLM key is configured:

bash
uv pip install "cognee[gliner]"

Run locally without an LLM

2. Build and query memory. With no LLM key configured, Cognee extracts the graph with the local GLiNER model and embeds with a local embedding model; both download on first use.

Save this as quickstart.py and run python quickstart.py if you are feeling old school, or tell your LLM to do it:

python
import asyncio

import cognee


async def main():
    # Extract a knowledge graph and embed the text with local models.
    await cognee.remember(
        "Marie Curie was born in Warsaw and worked at the University of Paris.",
        dataset_name="local_quickstart",
    )

    # Retrieve the matching source text; no LLM generates an answer.
    results = await cognee.recall(
        "Where was Marie Curie born?",
        datasets=["local_quickstart"],
    )
    for result in results:
        print(result)


if __name__ == "__main__":
    asyncio.run(main())

The same workflow is available from the CLI:

bash
cognee-cli remember "Marie Curie was born in Warsaw." -d local_quickstart
cognee-cli recall "Where was Marie Curie born?" -d local_quickstart

Text ingestion, retrieval, and session storage work without an LLM. LLM-dependent improvement stages skip automatically.

Generated answers and media processing that requires a vision or transcription model need additional LLM configuration.

The bundled GLiNER extractor is a demo of Cognee's small-model pipeline. For a production-ready version with higher accuracy and broader label coverage, reach out to us.

Optional: Configure the LLM

3. Add an LLM to get generated answers instead of retrieved passages:

python
import os

os.environ["LLM_API_KEY"] = "YOUR OPENAI_API_KEY"

Alternatively, create a .env file using our template.

Once a key is set, Cognee uses OpenAI for language models and embeddings, and processing and generated answers make provider calls. See installation, other providers, or local Ollama models for other setups.

Explore the bundled demo

To explore a prebuilt graph without downloading extraction or embedding models:

bash
cognee-cli demo

This command works with the base pip install cognee package. It loads bundled sample data and runs keyword search without an API key. Use the local quickstart above to build a graph from your own text.

How Cognee works

Cognee builds connected memory from different sources. Text becomes entities, relationships, and searchable chunks; code becomes a graph of symbols and dependencies. Session distillation curates accepted lessons into permanent memory.

At query time, retrieval selects relevant graph, vector, or code context. Your application can inspect the retrieved evidence and use it to answer a question or continue an agent task.

Operation What it does Learn more
remember Store content or code in permanent memory, or in a session when a session ID is supplied. Store memory
recall Retrieve context and answers, using automatic routing or a chosen search strategy. Query memory
improve Enrich memory, apply feedback, and bridge session knowledge into the graph. Improve memory
forget Remove a specific item or dataset. Delete memory

Explore the architecture and session lifecycle.

Connect your agent

Install the Claude Code plugin:

bash
claude plugin marketplace add topoteretes/cognee-integrations
claude plugin install cognee-memory@cognee

Or install the Codex plugin. Enable hooks first, either with the CLI:

bash
codex features enable hooks

or in ~/.codex/config.toml:

toml
[features]
hooks = true

Then add the marketplace and the plugin:

bash
codex plugin marketplace add topoteretes/cognee-integrations --ref main
codex plugin add cognee@cognee

Follow the plugin setup guide to configure local or remote memory.

Interface Start here
Claude Code memory plugin Install and configure the plugin
OpenClaw memory plugin Install @cognee/cognee-openclaw
Cursor, Cline, and other MCP clients Cognee MCP guide and server README
Python applications Python API reference
TypeScript applications TypeScript SDK
Rust applications Cognee-RS
Applications using HTTP REST API reference

Browse the integrations repository for agent frameworks, plugins, and source connectors. Each guide describes its setup and memory capture behavior.

To inspect a local installation in the UI:

bash
cognee-cli -ui

The UI launcher requires Node.js/npm; Docker is needed for its MCP service. See local UI setup.

Explore examples

Deploy Cognee

For a local API demo using a prebuilt image, follow the minimal Docker Compose guide. It includes a persistent-volume configuration and explains the single-user demo settings. Or run the image directly:

bash
docker run --rm -it -p 8000:8000 \
  -e LLM_API_KEY="sk-..." \
  -e ENABLE_BACKEND_ACCESS_CONTROL=false \
  -v cognee_storage:/cognee-storage \
  cognee/cognee:main

ENABLE_BACKEND_ACCESS_CONTROL=false is the single-user/local posture โ€” without it the API defaults to multi-tenant mode and every /api/v1 call requires an authenticated user. To keep authentication on instead, set DEFAULT_USER_PASSWORD to make the default account loginable (see the compose guide). Note that --rm discards container-local data on exit; the -v cognee_storage:/cognee-storage named volume keeps your memory across runs.

To run the API, UI, and MCP server from a source checkout, clone this repository, enter its directory, copy .env.template to .env, and configure your providers. Then run:

bash
docker compose --profile ui --profile mcp up

The default ports are API 8000, UI 3000, and MCP 8001. For deployment beyond a local demo, configure authentication, persistent storage, and compatible backends using the permissions guide and deployment templates. Cognee Cloud provides the managed option.

The cognee/cognee image ships with the GLiNER runtime baked in, so text ingestion and retrieval work in Docker without an LLM key; the local extraction and embedding models download on first use. Set LLM_API_KEY (as above) when you want generated answers.

Run the Whole Memory Layer on Postgres

Graph memory traditionally means operating a stack โ€” a graph database for relationships, a vector database for embeddings, Redis for sessions, and a relational database for metadata โ€” all deployed, secured, and paid for before an agent remembers anything. Since cognee 1.0 you can run the entire memory layer on a single Postgres instance.

โš ๏ธ Warning: Using Postgres as a graph store is currently released as a demo feature. The production-ready version is available as a licensed product. Use the demo to keep relational metadata, PGVector, and graph state working together in one Postgres service.

Benchmarks and research

The BEAM evaluation measures conversational memory using synthetic long-context conversations and an LLM judge. The reported runs use Cognee's memory components with benchmark-specific data formatting, prompts, and retrieval configuration.

BEAM context Reported score (0โ€“1) Scope
100K tokens 0.79 Fixed hybrid retrieval; four evaluation rounds over 20 questions from one held-out conversation.
10M tokens 0.67 Exploratory result; question-type routing selected and scored on the same question set, averaged over five rounds.

The two settings use different conversations, ingestion models, and retrieval-selection procedures. Read the methodology, models, limitations, and reproduction instructions before comparing these scores with other systems. The report also documents the remaining reproduction gap for the distributed 10M ingestion.

For the research behind Cognee's graph/LLM interface, see Optimizing the Interface Between Knowledge Graphs and LLMs for Complex Reasoning (Markovic et al., 2025).

Latest News

  • v1.6.1 โ€” Google Sync & Visualization (September 24, 2026): Google Drive and Gmail OAuth connectors bundled in the SDK, /visualize/json streamed in chunks for large graphs, and the GLiNER installer moved off the event loop with CPU torch installed on first use.
  • Document external_metadata is stamped on every chunk and surfaced in hybrid retrieval. Breaking: dlt is now a core dependency โ€” make sure it is installed if you manage dependencies by hand.
  • v1.6.0 โ€” Keyless workflows & pipeline reliability (September 18, 2026): build and search text memory with local models and no cloud LLM key; LLM-dependent improvement stages skip when no LLM is configured, and pipeline recovery preserves completed documents after crashes.

Community & Support

Contributing

We welcome contributions from the community! Your input helps make Cognee better for everyone. See CONTRIBUTING.md to get started.

Code of Conduct

We're committed to fostering an inclusive and respectful community. Read our Code of Conduct for guidelines.

Research & Citation

We recently published a research paper on optimizing knowledge graphs for LLM reasoning:

bibtex
@misc{markovic2025optimizinginterfaceknowledgegraphs,
      title={Optimizing the Interface Between Knowledge Graphs and LLMs for Complex Reasoning},
      author={Vasilije Markovic and Lazar Obradovic and Laszlo Hajdu and Jovan Pavlovic},
      year={2025},
      eprint={2505.24478},
      archivePrefix={arXiv},
      primaryClass={cs.AI},
      url={https://arxiv.org/abs/2505.24478},
}

Docker Colima Setup (`docs/docker-colima-setup.md`)

Docker & Colima Setup for Cognee UI / MCP

The cognee-cli -ui command starts an MCP server inside a Docker container.
This requires a running Docker-compatible daemon. Both Docker Desktop and
Colima (an open-source, commercially-free alternative) are supported.

Option A: Docker Desktop

Install from https://www.docker.com/products/docker-desktop/ and start the
application. No extra configuration is needed.

Option B: Colima (macOS / Linux)

Colima provides a lightweight container
runtime without a Docker Desktop licence.

Install

bash
# macOS (Homebrew)
brew install colima docker

# Linux (Homebrew)
brew install colima docker

Start Colima

bash
# Basic start
colima start

# Recommended: give the VM a host-reachable network address
colima start --network-address

Important: --network-address does not itself add a host.docker.internal
DNS entry โ€” it provisions a shared-network IP that is reachable from both the
Colima VM and the host. Colima maps host.docker.internal โ†’
host.lima.internal by default (recent versions), so combined with a
reachable address host.docker.internal generally resolves; directly
resolving it to the --network-address IP is still an open request
(abiosoft/colima#560). The
cognee MCP entrypoint also includes automatic fallback logic (tries
host.docker.internal, then host.lima.internal, then the container's
default gateway IP), so the host-API flow works even without this flag.

Verify

bash
docker info   # Should print server information without errors
docker run --rm hello-world

Troubleshooting

"Docker daemon is not responding"

Runtime Fix
Docker Desktop Open the Docker Desktop application and wait for the engine to start.
Colima Run colima start (or colima start --network-address).
Linux systemd Run sudo systemctl start docker.

Container cannot reach host API (`localhost` / `127.0.0.1`)

Inside a container, localhost refers to the container itself, not the host
machine. The MCP entrypoint automatically rewrites localhost / 127.0.0.1
to a reachable host address using the following fallback order:

  1. host.docker.internal (Docker Desktop on macOS / Windows / Linux)
  2. host.lima.internal (Colima / Lima)
  3. Default gateway IP (plain Linux Docker, typically 172.17.0.1)

If none of these work:

bash
# Use the Docker bridge gateway directly
docker run -e API_URL=http://172.17.0.1:8000 ...

# Or use host networking (Linux only)
docker run --network host ...

Colima: `host.docker.internal` does not resolve

First, give the VM a host-reachable address (most setups need only this):

bash
colima stop
colima start --network-address

This provisions a VM IP reachable from the host; host.docker.internal then
typically resolves via Colima's default host.docker.internal โ†’
host.lima.internal mapping. Note that --network-address does not itself
write a DNS entry.

If the name still does not resolve (e.g. a missing or custom mapping), add it
explicitly via network.dnsHosts in ~/.colima/default/colima.yaml:

yaml
network:
  dnsHosts:
    host.docker.internal: host.lima.internal

then colima restart.


Minimal Docker Compose (`docs/minimal-docker-compose.md`)

Minimal docker-compose for a local try-out

Try the Cognee API server with a single copy-pasteable file โ€” no cloning, no
building. It uses the prebuilt cognee/cognee
image with the default local databases (SQLite, LanceDB, Ladybug), so the only
thing you need to provide is an LLM API key.

Prerequisites

  • Docker with the Compose plugin (Docker Desktop, Colima, or any OCI-compatible
    runtime โ€” see Docker & Colima Setup)
  • An OpenAI API key (the default LLM and embedding provider)

1. Save this as `docker-compose.yml` in an empty directory

yaml
services:
  cognee:
    image: cognee/cognee:main
    ports:
      - "8000:8000"
    environment:
      LLM_API_KEY: ${LLM_API_KEY:?set LLM_API_KEY to your OpenAI API key}
      # Single-user try-out: no auth, shared local databases.
      # Remove this line (or set it to true) for multi-tenant mode,
      # which requires authentication on every API call.
      ENABLE_BACKEND_ACCESS_CONTROL: "false"

2. Start it

bash
export LLM_API_KEY="sk-..."   # your OpenAI API key
docker compose up

3. Verify it works

bash
curl http://localhost:8000/health

Then open http://localhost:8000/docs for the interactive API reference and
send your first requests:

bash
# Ingest a text file
echo "Cognee turns documents into AI memory." > note.txt
curl -X POST http://localhost:8000/api/v1/add \
  -F "[email protected]" \
  -F "datasetName=main_dataset"

# Build the knowledge graph
curl -X POST http://localhost:8000/api/v1/cognify \
  -H "Content-Type: application/json" \
  -d '{"datasets": ["main_dataset"]}'

# Search it
curl -X POST http://localhost:8000/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"searchType": "GRAPH_COMPLETION", "query": "What does Cognee do?", "datasets": ["main_dataset"]}'

Keeping data across restarts

The minimal file above stores everything inside the container, so removing the
container removes your data. To persist it, mount a named volume at the
image's built-in storage path:

yaml
services:
  cognee:
    image: cognee/cognee:main
    ports:
      - "8000:8000"
    environment:
      LLM_API_KEY: ${LLM_API_KEY:?set LLM_API_KEY to your OpenAI API key}
      ENABLE_BACKEND_ACCESS_CONTROL: "false"
    volumes:
      - cognee_storage:/cognee-storage

volumes:
  cognee_storage:

Note: /cognee-storage is the authoritative storage path baked into the
image (its Dockerfile defaults DATA_ROOT_DIRECTORY and
SYSTEM_ROOT_DIRECTORY under it, pre-created and owned by the non-root
cognee user, uid 1000) โ€” the same convention the repository's
docker-compose.yml uses. You can relocate storage
(e.g. to /cognee-data) by overriding DATA_ROOT_DIRECTORY and
SYSTEM_ROOT_DIRECTORY, but a fresh named volume mounted at a custom path is
created root-owned, so you must also make it writable for uid 1000.

Going further

  • Other LLM providers (Anthropic, Gemini, Ollama, โ€ฆ): add the matching
    LLM_PROVIDER / LLM_MODEL / LLM_ENDPOINT variables โ€” see
    .env.template for the full list.
  • UI, MCP server, Postgres, Neo4j: the repository's
    docker-compose.yml provides these as opt-in
    profiles โ€” see Run with Docker in the README.
  • Production: multi-tenant mode (ENABLE_BACKEND_ACCESS_CONTROL=true, the
    default) requires authentication and isolates data per user and dataset.
    Review the security variables in .env.template before
    exposing the API.

Ollama Models (`docs/ollama_models.md`)

Supported Ollama Models for Structured Graph Extraction

Cognee supports using local Large Language Models (LLMs) via Ollama. However, because Cognee relies on structured output generation (using Instructor with JSON schemas) to extract knowledge graphs, the performance and reliability of the extraction pipeline depend heavily on the model's capabilities.

This guide lists recommended models, models with known limitations, and troubleshooting tips.


Model Support Matrix

These models consistently format output correctly according to abstract JSON schemas, making them highly reliable for Cognee's graph extraction:

  • Llama 3.1 (8B, 70B) (e.g., llama3.1:8b, llama3.1:70b) โ€” Highly Recommended
  • Llama 3.2 (3B) (e.g., llama3.2:3b) โ€” Recommended for lightweight or resource-constrained environments.
  • Llama 3.3 (70B) (e.g., llama3.3) โ€” Outstanding extraction capability if hardware permits.
  • Qwen 2.5 (14B, 32B, 72B) (e.g., qwen2.5:14b, qwen2.5:32b, qwen2.5:72b) โ€” Strong extraction and reasoning capability.

2. Known Issues & Limitations

These models have high failure rates during structured JSON schema extraction. They often output invalid JSON, verbose conversational padding, or fail to follow abstract object definitions, leading to empty or dropped graphs:

  • Mistral (7B) (e.g., mistral, mistral:7b) โ€” Unstable structured JSON output, prone to schema format violations.
  • Phi 3 / Phi 3.5 (e.g., phi3, phi3.5) โ€” Fails to consistently adhere to Pydantic schemas.
  • Qwen 2.5 (7B and smaller) (e.g., qwen2.5:7b, qwen2.5:3b, qwen2.5:1.5b) โ€” Struggles with complex schemas compared to the larger $14\text{B}+$ variants.
  • Gemma 2 (2B, 9B) (e.g., gemma2:2b, gemma2:9b) โ€” Prone to schema validation drops.

3. Unknown / Experimental Models

Any model not listed above is treated as unvalidated/experimental. If you choose to run an unvalidated model, Cognee will emit a warning but will not block execution.


Troubleshooting Local Extraction

If you notice that cognify() is running but your final queries yield empty search results or no nodes are created, check the following:

  1. Verify your Model: Ensure you are using one of the recommended models (e.g., llama3.1:8b).
  2. Check Temperature: Extraction runs at temperature 0.0 by default on Ollama, for deterministic output formatting. If you set LLM_TEMPERATURE to something higher, lower it back to 0.0 in your .env.
  3. Verify API Connection: Ensure Ollama is running and that LLM_ENDPOINT is the bare host, http://localhost:11434. The default litellm_native framework appends Ollama's own API paths, so the /v1 form returns a 404. Use http://localhost:11434/v1 only with STRUCTURED_OUTPUT_FRAMEWORK="instructor".
  4. Inspect Logging: Check the console log outputs. If Cognee catches validation errors during extraction, they will be reported as warnings.

Recall Vs Search (`docs/recall-vs-search.md`)

Both functions query the knowledge graph. recall() is the memory-API entry point and the one
to reach for by default; search() is the low-level operation it calls underneath. This page says
what recall() adds, when to drop down to search(), the two places where the same argument
means different things, and how the query router picks a strategy.

Source of truth: cognee/api/v1/recall/recall.py and cognee/api/v1/search/search.py.

  1. Query routing. When query_type is omitted and auto_route=True (the default), an
    ordered table of regex rules picks a SearchType โ€” first match wins, no LLM call, so
    routing is free. With auto_route=False and no query_type, recall uses
    HYBRID_COMPLETION. Passing query_type bypasses the router entirely. See
    The router below.
  2. Session memory as a source. scope selects where results come from: "graph" (the
    permanent graph via search()), "session" (Q&A entries in the session cache),
    "trace" (agent trace entries), "session_context" (the distilled guidance block), plus
    opt-in "tools" (authorized external databases) and "code" (the code graph). "auto" (the
    default) and "all" never imply "tools" or "code". With a bare session_id and no
    datasets/query_type, a session hit short-circuits the graph search; scope="session_first"
    asks for that short-circuit explicitly, with a pinned type and datasets in play.
  3. Normalized results. Every returned entry is tagged with a source key ("graph",
    "session", "trace", "session_context", "tools", "code", "skills"), so callers
    can tell where it came from, and graph entries also carry the search_type that actually
    ran. search() returns raw SearchResult objects.
  4. Skill gate. Procedural-sounding queries trigger a concurrent SKILLS lookup whose hits
    are appended tagged source="skills" (only when exactly one dataset is targeted; disable
    with SKILL_GATE_ENABLED=false).
  5. Structured output shorthand. response_model= validates the LLM answer against a
    Pydantic model and returns it in the result's structured field.

When to call search() directly

  • You need the agentic extras as first-class parameters: skills, tools, max_iter,
    code_query, node_type. (recall() reaches most of these through
    retriever_specific_config={...}, which you assemble yourself.)
  • You want raw SearchResult objects rather than source-tagged entries.
  • You want a pinned query_type with no router and no session layer in the path, for
    example when benchmarking one retriever.
  • You are writing a custom pipeline task or debugging a single retrieval stage.

Same argument, different meaning

Argument recall() search()
session_id Makes the session cache a source (and may short-circuit the graph) Only adds session history to the retrieval context; never searches the cache as a source
omitted query_type Router picks one; HYBRID_COMPLETION if routing is off Always HYBRID_COMPLETION
top_k default 15 default 15 (the CLI's recall --top-k defaults to 10)
only_context=True Same as search(); pin query_type so the hybrid retriever cannot defer to GRAPH_COMPLETION behind your back Returns what the LLM would have received instead of its answer: for completion types the user prompt (history, rendered question and context, session guidance) with the system prompt (the task template) alongside; retrieval-only types return their context

Quick reference

python
import cognee
from cognee import SearchType

# Ordinary retrieval: let recall route the query and tag the sources.
results = await cognee.recall("What did Alice work on?", datasets=["project"])

# Session-first: answers the current conversation from the cache before touching the graph.
results = await cognee.recall("what did I just say about deadlines?", session_id="chat_1")

# Pinned strategy, no router.
results = await cognee.recall("timeline of the migration", query_type=SearchType.TEMPORAL)

# Low level: raw SearchResult objects, agentic parameters as keywords.
raw = await cognee.search(
    "Which functions call UserService?",
    query_type=SearchType.CODE,
    code_query={"operation": "impact_analysis", "seeds": ["UserService"]},
)

The router

Source: cognee/api/v1/recall/query_router.py.

Rules are checked in order and the first match wins. Shape rules โ€” what the input looks
like
โ€” come first and win over intent rules, so a quoted string or a Cypher statement is
handled as what it is even when its text also reads as intent: "coding rules" is a lexical
search for that phrase, not a request for the rule list. No query in the golden table depends
on that ordering (a test enforces this). Anything unmatched goes to HYBRID_COMPLETION.
Matching is case-insensitive.

# Rule Signal in the query Routes to
1 quoted_phrase The whole query is one "quoted phrase" CHUNKS_LEXICAL
2 coding_rules_intent coding rules / coding standards / coding conventions, or code review guidelines (and the rules, standards, checklist, conventions variants) CODING_RULES
3 time_scoped_question A time preposition (in, before, after, since, between, from, on, during, as of, โ€ฆ) directly followed by an absolute date โ€” or for followed by one that ends the phrase (scheduled for 2031, but not for 2000 users): a four-digit year (in 2019, in early 2024), a decade (in the 1990s), a month and year (in July 1969), a full date (on 7 November 1867, on March 1, 2024), or an ISO date (on 2024-03-01) TEMPORAL
โ€” default Anything else HYBRID_COMPLETION

CYPHER is not in the table and never will be. The Cypher retriever runs
the query text verbatim through graph_engine.query(), and the whole recall
path checks read permission only โ€” so a routable CYPHER would let anyone who
can read a dataset destroy it by posting
{"query": "MATCH (n) DETACH DELETE n"}. Reaching CYPHER requires an
explicit query_type / searchType, which is a deliberate act by the caller
rather than whatever text arrived in a request body. Pasted Cypher therefore
routes to HYBRID_COMPLETION like any other text.

The rule for what belongs in the table: auto-routing may only pick a
strategy that is at least as good as HYBRID on a default-built graph and does
not add LLM calls without an unambiguous signal.

HYBRID_COMPLETION searches document chunks, pre-computed summaries, and the
entity neighbourhood in a single LLM call. Almost every alternative strategy
sees a subset of that context, sometimes with an extra round trip attached.
So each rule above fires on an input that is not a natural-language question at
all โ€” pasted Cypher, a fully quoted literal, a request for the rule list โ€” and
for which HYBRID is the wrong operation rather than a worse one. A rule that
fires on an ordinary question is a regression even when its target sounds
better suited.

That is why these are not auto-routed, even though they are valid
query_type values:

  • GRAPH_COMPLETION_COT runs up to four reasoning iterations. "Why" and
    "explain" questions are answered by the default strategy; pin
    query_type=SearchType.GRAPH_COMPLETION_COT when you want the loop.
  • GRAPH_COMPLETION_CONTEXT_EXTENSION adds traversal rounds. HYBRID already
    includes entity neighbourhoods, so "related to" questions stay on the
    default.
  • GRAPH_SUMMARY_COMPLETION does not read pre-computed summaries: it runs
    GRAPH_COMPLETION and then makes a second LLM call to summarize the answer.
    Routing "summarize the report" there would drop HYBRID's document and
    summary lanes and add a round trip.
  • TEMPORAL is routed, but only on an absolute date behind a time
    preposition. The default pipeline extracts Timestamp nodes, and the
    retriever reranks HYBRID's own candidates by the window โ€” it never sees less
    context than HYBRID โ€” so the only cost of the route is the one LLM call that
    reads the window out of the question. That call is wasted when there is no
    window to find, which is why a bare when, since monday, the 2019 report, ticket 2048 or Q4 2024 stay on the default. Known limitation: a
    four-digit quantity after a preposition (in 2048 bytes) reads as a year;
    the retriever then finds no window and answers as HYBRID. With no LLM
    configured, recall picks CHUNKS before the router runs, so a temporal
    question never reaches the interval extraction.
  • "Exact"/"verbatim" phrasing does not select CHUNKS_LEXICAL. BM25 tokenizes
    the raw query, so the trigger word itself becomes a rare, high-IDF search
    term and skews the ranking it was meant to sharpen. quoted_phrase has no
    such problem: its trigger is punctuation, which tokenization drops.
  • Incidental code tokens (def, import, class Foo(, .py, refactor,
    lint) do not select CODING_RULES. That retriever reads only the
    coding_agent_rules nodeset and returns nothing on an ordinary graph.

When a routed strategy comes up empty

A routed type is a guess, so recall() never lets one do worse than the
default. When the router picked a type other than the default, the query is
retried once as HYBRID_COMPLETION in two cases:

  • the backend rejects the type; or
  • the search returns nothing and the empty result means the lane was
    unavailable โ€” CHUNKS_LEXICAL with no lexical hits, CODING_RULES on a
    dataset with no rules nodeset.

The search history records the type that actually answered. Two things are
never second-guessed: a type you pinned yourself returns empty or raises as
before, and a failure of the default itself is raised rather than hidden โ€”
there is nothing left to fall back to, so the error is real.

Bypassing the router

Surface Route automatically Pin a strategy
SDK recall() omit query_type (default) pass query_type=SearchType.X; auto_route=False forces HYBRID_COMPLETION without routing
REST POST /api/v1/recall omit searchType or pass null (default) pass a value
CLI cognee-cli recall omit --query-type --query-type X (choices in cognee/cli/config.py:SEARCH_TYPE_CHOICES)

All three surfaces auto-route by default โ€” with one exception. When no usable
LLM key is configured, recall picks CHUNKS before the router is consulted,
because nothing can write a completion answer; the router never runs and an
explicit query_type is the only way to select a strategy. That branch keys
off LLM availability, not off the extractor that built the graph.

On every surface, omitting the type also makes the session a search source
whenever a session_id is given: alone it short-circuits the graph on a hit,
alongside datasets both contribute. Pinning a type leaves the graph as the only
source unless you ask for the session by name with scope. REST clients that relied on the old
HYBRID_COMPLETION default should pass "searchType": "HYBRID_COMPLETION"
explicitly.

Seeing what ran

Graph results carry the resolved type as search_type. The CLI prints it in
the Found N result(s) using ... line. The recall span carries the type as
cognee.search.type and, when the router chose it, the rule name as
cognee.recall.route_rule โ€” so which rule fires on real traffic is answerable
without reproducing the query. The router also logs the rule name at DEBUG
level, and never logs the query text.

Adding a rule

Add a (name, compiled pattern, SearchType) tuple to _RULES in
query_router.py, then add cases to the golden table and the negative
invariants in cognee/tests/unit/api/v1/recall/test_query_router.py. Two
structural tests constrain what you can add: the new target must be in
ROUTABLE_TYPES, and the new pattern must not change how any query in the
golden table routes. Keep the size principle above in mind: a rule that sends
ordinary questions to a slower or narrower retriever is a regression, not an
improvement.

Related: the search-type list in CLAUDE.md ("SEARCH: Retrieval"), examples/guides/recall_core.py,
examples/guides/hybrid_retrieval_recall.py.


Slack History (`docs/slack-history.md`)

Import Slack conversations into Cognee

The SDK's Slack integration can import selected channel history or complete
threads into an existing Cognee dataset. It uses native add, update,
cognify, and datasets.delete_data; the connector stores only selection and
run status alongside the existing encrypted Slack connection.

Install or upgrade the Slack app

  1. Configure the existing Cognee Slack OAuth connection. Add these bot scopes:
    channels:history, groups:history, and groups:read, alongside the existing
    commands, chat:write, im:write, and channels:read scopes. Reinstall or
    reconnect the app to grant the new scopes. Reading history is opt-in; granting
    scopes does not start an import.
  2. Invite the app to each selected channel. The Slack person who connected the
    workspace must also be a member. Bulk imports are managed by the connection
    owner; linking another member's Cognee account does not grant them the
    installation's ability to export channel history.
  3. Add /cognee-import with the same request URL as /cognee-remember:
    https://YOUR_API/api/v1/slack/commands.
  4. Add a message shortcut named Remember entire thread, callback ID
    remember_thread. Set the Interactivity request URL and the Options Load
    URL
    to https://YOUR_API/api/v1/slack/interactive. The options URL supplies
    destination datasets from the caller's actual Cognee permissions.
  5. Restart the updated API. Scheduled refreshes run inside the API process once
    a selection is enabled. SLACK_HISTORY_SYNC_ENABLED=false disables this
    worker for deployments that use an external scheduler instead.

The existing /cognee-remember command and single-message shortcut keep their
behavior. /cognee-ask already searches imported memory using Cognee's dataset
permissions. No frontend or MCP changes are required.

Select information in Slack

Run /cognee-import (seven days by default), or /cognee-import 14. The dialog
lets you choose a destination dataset, channels, thread links, and ongoing sync.
The message shortcut starts the same dialog with that entire thread selected.

The dataset picker offers datasets where the owner has read, write, and
delete
permission: native document replacement and source deletion require
those rights. The importer rechecks permissions before fetching and before
writing. A channel allowlist also applies to the selected source channels,
not only the channel where a command is invoked.

By default, dates select threads started during the period. The optional
active during the period mode scans older roots too, so a recent reply to an
old thread is included. This requires more Slack calls. In both modes the
selected conversation includes all currently available replies, even those
outside the date range. Thread links select complete conversations independently
of channel dates. Files/attachments and DMs are not imported; message text,
authors, timestamps and source links are retained. Cognee bot replies are skipped
to avoid feeding its answers back into its own source memory.

Confirmation is private and sent after indexing completes. Large imports may
outlive Slack's response URL or a server restart; use the status endpoint below
and retry an interrupted selection. Repeating an import does not duplicate its
documents.

Dataset and node-set organization

Use datasets as access boundaries and node sets to organize channels. Each
conversation receives slack, slack:<team_id>, and
slack:channel:<channel_id> node sets. Node sets do not enforce permissions.
Choose an appropriately restricted destination for private channels; source
membership is not copied into Cognee ACLs and this feature never grants access.

The importer manages only documents carrying its own source stamp in the target
dataset. Manually remembered Slack notes, other sources, and other datasets are
not swept by source reconciliation.

API and SDK

All HTTP endpoints require a Cognee bearer token/session belonging to the Slack
connection owner. JSON fields use snake_case.

text
POST /api/v1/slack/history/{team_id}/import
PUT  /api/v1/slack/history/{team_id}/sync
GET  /api/v1/slack/history/{team_id}

Example import request:

json
{
  "dataset_id": "YOUR_DATASET_UUID",
  "channel_ids": ["C0123456789"],
  "days": 14,
  "thread_mode": "active",
  "thread_links": []
}

Instead of days, use timezone-aware oldest and optional latest dates, for
example 2026-09-01T00:00:00Z. For selected conversations alone, omit
channel_ids and dates and supply thread_links, or structured
threads: [{"channel_id": "C0123456789", "ts": "1700000000.000001"}].

To configure periodic refresh, PUT:

json
{
  "enabled": true,
  "interval_seconds": 21600,
  "selection": {
    "dataset_id": "YOUR_DATASET_UUID",
    "channel_ids": ["C0123456789"],
    "days": 14
  }
}

A saved selection resolves days into a fixed start, then extends to the
present on each refresh. There is one selection per workspace and destination
dataset; saving it again replaces that selection. Separate destination datasets
can have different source selections. To disable one, PUT the same selection
with enabled: false. Disabling stops future runs, not an already running import.
GET returns saved selections and the last run's status/counts, never tokens or
message content. Failed refreshes become eligible for retry after ten minutes;
successful refreshes use the configured interval (six hours by default).

For an embedded SDK application, the same entry points are:

python
from cognee.modules.integrations.slack.history import import_slack_history
from cognee.modules.integrations.slack.history_models import SlackHistoryRequest

result = await import_slack_history(
    team_id,
    SlackHistoryRequest(dataset_id=dataset_id, channel_ids=channel_ids, days=14),
    user=owner,
)

Use the HTTP API when Cognee already runs in another process. The runnable
HTTP example avoids opening
the server's embedded databases in a second process.

Sync guarantees and limits

  • Every channel/thread page is fetched before native writes begin. API errors,
    missing pagination cursors, and request/message caps fail the import rather
    than marking a partial snapshot complete. Defaults are 1,000 requests and
    50,000 messages; API callers can explicitly raise these bounded limits.
  • Stable identity includes destination dataset, Slack workspace, channel, and
    root timestamp. Changed threads go through native update, so stale derived
    graph/vector content is removed. Identical imports reuse the same documents.
  • Apps using rotating Slack tokens refresh expiring credentials before history
    reads. Rotation preserves source selections, channel restrictions and account
    ownership; it cannot reactivate a concurrently revoked connection.
  • Scheduled reconciliation also re-fetches previously imported conversations
    in the selected channels, catching edits, deleted replies, late replies, and
    changes made while the API was offline. A confirmed missing thread deletes
    only its corresponding source document through native deletion. Lost scopes,
    channel access, and transient errors never stand in for deletion.
  • A rolling date window is not retention. A shorter import never deletes
    older documents just because they are absent from that window. Reconciliation
    reflects what Slack currently makes available; it cannot recover history
    already removed by Slack retention policies.
  • Native multi-document updates are retryable, not atomic across the complete
    import. If indexing fails after ingestion, retrying still runs indexing even
    when raw content is unchanged. One-off imports interrupted by process exit
    require a retry; enabled scheduled selections recover on a later tick.
  • Run one API worker with local embedded stores. Native dataset locks and this
    refresh worker are process-local; a multi-worker deployment must designate a
    single history worker and serialize external import requests.

Slack API references: history,
threads,
rate limits. Internal customer
apps retain the normal history tier; stricter limits apply to some commercially
distributed apps. The connector honors Retry-After and bounded retries.


Supply Chain Provenance (`docs/supply_chain_provenance.md`)

Supply-chain provenance & release attestations

Cognee's release pipeline produces verifiable provenance for every shipped
artifact so consumers can independently confirm that a package on PyPI or an
image on Docker Hub was built from this repository by our CI โ€” not tampered with
in transit or rebuilt by a third party.

This covers three layers of evidence:

Artifact Mechanism Where it is recorded
PyPI sdist + wheel PEP 740 digital attestations via PyPI Trusted Publishing PyPI project page ("Provenance" / "Attestations")
PyPI sdist + wheel SLSA build provenance (actions/attest-build-provenance) GitHub repo โ†’ Attestations tab
Docker images in-toto provenance + SBOM (buildx provenance/sbom) Pushed alongside the image manifest

The relevant workflows are .github/workflows/release.yml (tagged library releases),
.github/workflows/dev_canary_release.yml (weekly dev canaries), and
.github/workflows/release_mcp.yml (manual MCP releases).


One-time setup: PyPI Trusted Publishing

PyPI only accepts and displays PEP 740 attestations when a package is uploaded
through Trusted Publishing (OpenID Connect), not an API token. The library
workflows have already been switched to OIDC (id-token: write, no
UV_PUBLISH_TOKEN), but a project owner must register the trusted publishers
on PyPI once. release_mcp.yml is the exception until its publisher exists:
see cognee-mcp still uploads with a token.

PyPI scopes trusted publishers per project. Register three publishers, one
per workflow: two on cognee and one on the separate cognee-mcp project.

  1. Open the publishing settings for cognee
    and cognee-mcp.

  2. Under Add a new publisher โ†’ GitHub, add each publisher to the
    PyPI project listed below:

    Field Release publisher Canary publisher MCP release publisher
    PyPI project cognee cognee cognee-mcp
    Owner topoteretes topoteretes topoteretes
    Repository cognee cognee cognee
    Workflow name release.yml dev_canary_release.yml release_mcp.yml
    Environment (leave blank) (leave blank) (leave blank)

The Environment value must match the environment: declared on the
publishing job. The workflows do not set one, so leave this blank โ€” if you
later add a GitHub Actions environment, set the same name on both sides or the
OIDC publish step fails auth.

Optional hardening (not configured): a GitHub Actions environment with
required reviewers / branch restrictions can gate publishing so commit access
alone does not grant PyPI publishing rights. To enable it, add
environment: <name> to the publishing job and set the matching name on the
PyPI publisher above. Note that required reviewers on the canary workflow
would block its weekly cron.

  1. Save all three publishers.

Release MCP by running release_mcp.yml from the main branch in the Actions
tab. Other refs fail explicitly. The workflow reads the version from
cognee-mcp/pyproject.toml, refuses to run if that version is already on PyPI,
uploads it, and tags the commit cognee-mcp-v<version> (its own namespace,
since cognee-mcp versions independently of the library).

After the publishers are registered, the next library release uploads with
provenance automatically.

โš ๏ธ Do not run a library release before its publishers are registered โ€” the
publish step will fail OIDC auth. The release workflow is
workflow_dispatch-only, so you control the timing.

cognee-mcp still uploads with a token

The cognee-mcp publisher in the table above has not been registered, and only
the owner of that PyPI project can add it. Until then release_mcp.yml uploads
with the PYPI_TOKEN repository secret, the account-wide token the library used
before it moved to OIDC. Two consequences:

  • cognee-mcp files on PyPI carry no PEP 740 attestations. That includes 0.5.6,
    which was uploaded by hand with the same token on 2026-10-01 after the first
    OIDC run failed with invalid-publisher. The SLSA build provenance on GitHub
    is still produced for every workflow release.
  • The token can publish every project its account owns, cognee included, so
    it is a broader credential than this workflow needs.

Moving over needs no workflow change, because the publish step uses Trusted
Publishing whenever the secret is absent. Do it in this order:

  1. Register the cognee-mcp publisher from the table above.
  2. Delete the PYPI_TOKEN secret.

The order matters. Deleting the secret first leaves MCP releases with no way to
authenticate.


Verifying provenance as a consumer

PyPI package (PEP 740)

pip surfaces attestations from the PyPI "Provenance" section on the project /
file pages. You can also fetch the integrity/provenance metadata via the PyPI
JSON API:

bash
curl -s https://pypi.org/pypi/cognee/json | jq '.urls[].provenance'

PyPI package (SLSA, GitHub-hosted)

Download a wheel/sdist and verify the GitHub-hosted build provenance with the
GitHub CLI:

bash
gh attestation verify ./cognee-<version>-py3-none-any.whl --repo topoteretes/cognee

A successful verification confirms the artifact's SHA-256 digest was produced by
a workflow in topoteretes/cognee.

Docker image (in-toto provenance + SBOM)

bash
# Provenance attestation
docker buildx imagetools inspect cognee/cognee:latest \
  --format '{{ json .Provenance }}'

# SBOM attestation
docker buildx imagetools inspect cognee/cognee:latest \
  --format '{{ json .SBOM }}'

How this maps to trust signals

  • Package provenance (HVTracker / supply-chain trackers): flips from None
    to present once Trusted Publishing uploads PEP 740 attestations.
  • OpenSSF Scorecard
    • Signed-Releases โ€” satisfied by attested PyPI artifacts.
    • Token-Permissions โ€” release workflows declare a minimal top-level
      permissions: contents: read and opt into id-token/attestations only
      where needed.
    • Pinned-Dependencies โ€” all actions in the release workflows are pinned to
      full commit SHAs (with a version comment).

When bumping a pinned action, update both the SHA and its trailing
# vX.Y.Z comment together.


Turso Local (`docs/turso-local.md`)

Running cognee on Turso (local)

Turso is a rewrite of SQLite in Rust that keeps the SQLite file format and SQL dialect and adds
multi-version concurrency control (MVCC) and vector distance functions. cognee can run all four
of its stores on it, as local database files:

Layer Provider setting What it stores Adapter
Relational DB_PROVIDER=turso users, datasets, pipeline runs, ACLs cognee/infrastructure/databases/relational/sqlalchemy/TursoAdapter.py
Graph GRAPH_DATABASE_PROVIDER=turso graph-as-tables (graph_node, graph_edge) cognee/infrastructure/databases/graph/turso/adapter.py
Vector VECTOR_DB_PROVIDER=turso one table per collection, F32_BLOB embeddings cognee/infrastructure/databases/vector/turso/TursoVectorAdapter.py
Session cache CACHE_BACKEND=turso session Q&A, traces, context, usage logs (cache.db) cognee/infrastructure/databases/cache/sql/SqlCacheAdapter.py

The four layers are independent: any one of them can be Turso while the others stay on their defaults.
The session cache mirrors the sqlite backend exactly (a cache.db next to the relational
database, same tables and upserts) and only swaps the driver; it is not switched automatically
with DB_PROVIDER, so set CACHE_BACKEND=turso explicitly, as with postgres.
All three share cognee/infrastructure/databases/turso/: the SQLAlchemy dialect
(sqlite+cognee_turso://), the TURSO_* settings, the transaction/retry policy and the
file-cleanup rule.

Versions

Component Version Notes
pyturso (import name turso) >=0.7.2,<0.8 (pinned in the turso extra) the Turso rewrite engine and its Python bindings; SELECT turso_version() proves it is the executing engine
SQL dialect tracked by the engine SQLite 3.50.4 SELECT sqlite_version()
SQLAlchemy 2.0.x (cognee's pin) the bundled sqlite+aioturso dialect fails on 2.0.4x+, see "Upstream findings"
Python 3.10 โ€“ 3.14 wheels for Linux x86_64/aarch64, macOS, Windows

pyturso 0.8.0 release candidates exist. They add recursive CTEs, but 0.8.0rc11 aborts the
process on the neighborhood query cognee used, so cognee stays on 0.7.x and avoids recursive CTEs.

Setup

bash
pip install cognee"[turso]"          # or: uv pip install -e ".[turso]"

# .env โ€” pick any subset
DB_PROVIDER=turso
GRAPH_DATABASE_PROVIDER=turso
VECTOR_DB_PROVIDER=turso
CACHE_BACKEND=turso
LLM_API_KEY=...                       # cognify needs an LLM as usual

Files land under the system databases directory (SYSTEM_ROOT_DIRECTORY/databases): the
relational database as DB_NAME, and with access control on (the default) one graph file
(graph_<dataset_id>.db) and one vector file (<dataset_id>.turso.db) per dataset. Paths can be
pinned with GRAPH_DATABASE_URL / VECTOR_DB_URL (absolute paths). Remote Turso databases are
not supported in this version: a set DB_TURSO_URL, GRAPH_DATABASE_KEY or a libsql:// /
https:// vector URL is a hard error, never a silent fallback to a local file.

Engine-wide settings (TursoConfig, env prefix TURSO_):

Setting Default Meaning
TURSO_JOURNAL_MODE wal wal: SQLite-compatible write-ahead log, one writer at a time. mvcc: Turso's concurrent writes for the graph and vector stores (below); the relational DB and session cache always stay on wal.
TURSO_BUSY_TIMEOUT_MS 120000 how long a writer waits for the write lock in wal mode before database is locked
TURSO_CONFLICT_RETRIES 5 how many times the graph and vector write paths re-run a transaction that hit a write conflict

Runnable example

bash
uv run python examples/guides/turso_local_example.py ingest    # add -> cognify -> search
uv run python examples/guides/turso_local_example.py verify    # fresh process: search the stored files
uv run python examples/guides/turso_local_example.py cleanup   # forget(everything=True) + remove files

The script sets the three providers itself and keeps its files under .turso_example/ next to it.
verify runs in a new process and answers from the files ingest wrote, which is the persistence
check. TURSO_JOURNAL_MODE=mvcc runs the same flow with the graph and vector stores on concurrent
writes.

Migrating from the previous `turso` provider

Before this version the turso provider ran on plain SQLite (aiosqlite) for the relational and
graph layers and on libsql-experimental for vectors. Existing local files need no migration: a
WAL-mode file written by stock SQLite opens on the Turso engine and vice versa (verified both
ways). The remote replica mode of the old vector and relational adapters is gone; configure a local
file or another provider. Setting DB_TURSO_URL / DB_TURSO_AUTH_TOKEN is now a hard error, and the
turso extra no longer installs libsql-experimental.

A Turso file can also be open in only one process at a time. The old aiosqlite-based relational
and graph providers allowed several processes on one file, so a deployment that ran the API server
next to a separate SDK/CLI process, or several server workers, on the same files breaks on upgrade.

Transactions and concurrent writes

wal mode is SQLite's behaviour: writers take one lock, others wait up to TURSO_BUSY_TIMEOUT_MS
and then fail with database is locked. Files stay readable by stock SQLite. The driver's implicit
BEGIN only starts at the first INSERT/UPDATE/DELETE, so the graph and vector adapters open their
write transactions with BEGIN IMMEDIATE themselves (write_transaction()): a read-modify-write
(tag merges and removals, payload updates) holds the write lock from its first read, and a
concurrent writer waits and then reads its commit instead of being overwritten from a stale read.

mvcc mode (PRAGMA journal_mode=mvcc, applied on every connection) makes the graph and
vector
adapters open write transactions with BEGIN CONCURRENT. Independent connections commit
in parallel; only two transactions that write the same row conflict, and the engine reports
that eagerly, at the statement, as Write-write conflict, aborting the later transaction. Every
graph and vector write is one self-contained transaction, which the adapters re-run
(retry_on_conflict, jittered backoff, TURSO_CONFLICT_RETRIES attempts).

The relational DB and the session cache ignore mvcc and always run in wal
(TursoConfig.wal_only()). Their writes go through ORM sessions whose callers run arbitrary code
between statements, so a conflicted transaction cannot be replayed: under mvcc the second of two
writers to one row (pipeline status, dataset rows, the session-context upsert) would fail and its
write would be lost. Under wal it waits on busy_timeout and succeeds. The price is write
throughput under heavy concurrency: in a local benchmark of concurrent session-cache writes in one
process, wal fell from about 1,200 to about 150 writes/s at 64 writers, while mvcc held about
1,200 on distinct rows but, without retries, lost most same-row writes.

Constraints of the mode:

  • DDL needs a plain BEGIN ("DDL statements require an exclusive transaction"); cognee runs schema
    creation and migrations inside exclusive_transaction(), which switches the statement.
  • The database file gains a -log companion and is no longer readable by stock sqlite3
    (cognee's dataset cleanup removes -wal, -shm and -log). Switching a file back to wal
    restores SQLite compatibility. Migration c3d5e7f9a1b2, which heals a standalone cache.db,
    opens a Turso cache through pyturso, so it also handles a cache file left in mvcc mode.
  • Plain BEGIN writers still serialize with database is locked, so mixing tools that do not use
    BEGIN CONCURRENT gains nothing.
  • MVCC is experimental upstream. The default stays wal.

Measured in cognee/tests/e2e/turso/test_concurrent_writes.py (both modes): 6 writers on
independent connections, 40 rows each, through the graph adapter, the vector adapter and raw
threads; a forced same-row conflict; retry recovery; and a reopen after the writes. The relational
adapter's tests pin that two same-row writers both commit with TURSO_JOURNAL_MODE=mvcc set.

Test commands

bash
# Adapter-level, no LLM needed (also the CI job "Turso Tests")
pytest cognee/tests/unit/infrastructure/databases/turso_backend \
       cognee/tests/unit/infrastructure/databases/relational/test_turso_adapter.py \
       cognee/tests/unit/infrastructure/databases/relational/test_create_relational_engine.py \
       cognee/tests/unit/infrastructure/databases/graph/test_turso_graph_dataset_database_handler.py \
       cognee/tests/unit/infrastructure/databases/vector/test_turso_adapter.py \
       cognee/tests/unit/infrastructure/databases/cache/test_sql_cache_turso.py \
       cognee/tests/unit/infrastructure/databases/cache/test_sql_adapter_crud.py \
       cognee/tests/e2e/turso/test_turso_adapter.py \
       cognee/tests/e2e/turso/test_concurrent_writes.py

# The same adapter tests on concurrent writes (CI runs both modes)
TURSO_JOURNAL_MODE=mvcc pytest <the paths above>

# Full search suite with every layer on Turso (needs LLM + embedding keys)
DB_PROVIDER=turso GRAPH_DATABASE_PROVIDER=turso VECTOR_DB_PROVIDER=turso CACHE_BACKEND=turso \
  pytest cognee/tests/test_search_db.py -v

Coverage: the adapter tests run in both wal and mvcc in CI. The end-to-end search suites run
in wal only, so mvcc is not covered end to end.

Every Turso test asserts SELECT turso_version() (or the cognee_turso driver name), so a test
that silently ran on SQLite would fail.

Known limitations

Limitation Effect on cognee Handling
No approximate vector index (libsql_vector_idx / vector_top_k are not supported) similarity search is an exact vector_distance_cos scan ~16โ€“20 ms for top-15 over 20k ร— 384-dim rows on a laptop; fine for local datasets, not for millions of rows
No recursive CTEs (0.7.x) k-hop neighborhoods, connected components one query per hop; union-find in Python
No scalar subquery in ON CONFLICT DO UPDATE SET vector upsert that merged belongs_to_set in SQL tags are read and merged in Python, then a plain excluded.payload upsert
json_each().value returns a string element's raw JSON text, escapes included (B\u00fccher for Bรผcher); payloads are written with json.dumps, which escapes non-ASCII characters, quotes and backslashes node set names in vector search filters and tag removal names are compared on the decoded element (payload ->> je.fullkey); tag removal filters the names in Python inside one write transaction
Bind parameters must be None, numbers, str or bytes (no register_adapter) raw text() statements binding datetime/UUID SQLAlchemy-typed columns are unaffected; raw statements use a typed bindparam
Quoted identifiers are stored lowercased in sqlite_master has_collection by exact name, PascalCase collection detection case-insensitive lookup; collections detected by schema (id, payload, vector)
Parenthesized joins in a FROM clause (JOIN (a JOIN b ON โ€ฆ)) are rejected SQLAlchemy emits them for joined-table inheritance (User/Tenant are Principal subclasses) the dialect's compiler (turso/compiler.py) flattens the tree into a left-deep join chain
A join chain that combines several inner joins, a constant filter and a LEFT JOIN can drop a join condition and return extra rows; an affected query returns the wrong rows on every run none known: no current cognee query has the shape, but users and permissions are read through joins on this engine avoid the shape in new SQL, or check its rows against stock SQLite; test_turso_engine_bugs.py reproduces the smallest case and starts failing once a pyturso release fixes it
VACUUM requires an experimental engine flag none (cognee never runs VACUUM) โ€“
MVCC files are unreadable by stock SQLite external tooling on a live mvcc database switch the file to wal first
One synchronous connection used from two threads at once aborts the process (Rust panic) the vector adapter shares one connection every call runs under _connection_lock; keep it that way
A database file can be open in one process at a time, in both journal modes. The engine holds the file lock until that process exits (closing its connections does not release it), and it also blocks stock sqlite3 the API server and a separate SDK/CLI process, or several server workers, cannot share one Turso deployment; the stock SQLite backends allow this. cognee raises TursoDatabaseInUseError, naming the file, instead of the engine's Locking error run one cognee process per set of Turso files (the server's default gunicorn -w 1 is fine), or give each process its own SYSTEM_ROOT_DIRECTORY

Upstream findings

Each finding will be filed upstream with a minimal reproduction and linked from SDK-664. When a
finding is fixed in a released pyturso, the matching workaround (noted in the table above) can
be retired.

  1. sqlite+aioturso dialect fails on SQLAlchemy 2.0.4x+: 'AsyncAdapt_turso_dbapi' object has no attribute 'has_stop'. cognee's dialect sets it.
  2. The dialect's reflection mixin returns [] for indexes, foreign keys and unique constraints
    although PRAGMA index_list / foreign_key_list work; with it, cognee's idempotent migrations
    collide on existing indexes and Alembic batch operations would drop constraints. cognee's
    dialect restores stock SQLite reflection. Fixed upstream in the 0.8.0 release candidates.
  3. Scalar subquery in ON CONFLICT DO UPDATE SET: Parse error: Subquery is not supported in this position (0.7.2 and 0.8.0rc11).
  4. An outer json_each() over an unaliased column is misresolved inside UPDATE/DELETE: on
    0.7.2 the tag-removal UPDATE ... json_each(payload, ...) runs without error but changes no rows
    (stock SQLite strips the tag), and qualifying the column with a quoted mixed-case table name fails
    with no such table (finding 6). Aliasing the table (AS t, json_each(t.payload, ...)) works.
    Earlier shapes failed with bind index 1 is out of bounds; 0.8.0rc11 reports 'json_each' is not a function.
  5. WITH RECURSIVE: Recursive CTEs are not yet supported (0.7.2); 0.8.0rc11 accepts the syntax
    but aborts the process (Fatal Python error: Abort) on cognee's neighborhood query.
  6. Quoted identifiers lowercased in sqlite_master (stock SQLite preserves case).
  7. Only primitive bind parameter types; no register_adapter equivalent.
  8. JOIN (a JOIN b ON โ€ฆ): Parenthesized FROM clause subqueries are not supported, although
    the engine reports SQLite 3.50 (SQLite has accepted the form since 3.7.16).
  9. json_each().value returns a string element's raw JSON text instead of the decoded string
    (0.7.2): for the stored array ["B\u00fccher"] (how json.dumps writes ["Bรผcher"]) it
    returns the escape text B\u00fccher itself, where stock SQLite returns Bรผcher.
    payload ->> je.fullkey and json_extract(payload, je.fullkey) decode correctly.
  10. A join condition is dropped (0.7.2): with u=(2,1), a=(1,1), p=(3,1) and b empty
    (id INTEGER PRIMARY KEY, k INTEGER), SELECT u.id, a.id FROM u JOIN a ON u.id = a.id AND a.k = 1 JOIN p ON p.k = a.k LEFT JOIN b ON u.k = b.k returns (2, 1); stock SQLite returns
    no rows. Removing the constant filter, the third inner table or the LEFT JOIN gives the
    right result; moving the conditions to WHERE does not.

Contributing Add Catalog Entry (`docs/contributing/add-catalog-entry.md`)

Adding a catalog entry

The Cognee Integrations Hub and Use-Case Gallery are generated from the YAML entries under catalog/entries/. Adding a new integration, use-case, or community package is a single-PR change: catalog entry, plus a runnable example the entry points at.

This guide covers the mechanics. The contract itself is catalog/schema.json.

What the catalog is for

Users looking to adopt cognee ask two questions:

  • "Does cognee work with my stack?" (answered by the Integrations Hub)
  • "How do I do the thing I need done?" (answered by the Use-Case Gallery)

Every catalog entry is one card in one of those views. It carries just enough context for a user to decide it's the right starting point, plus a runnable example they can copy-paste.

Anatomy of an entry

Every entry is a YAML file under catalog/entries/{integrations,use-cases,packages}/. The filename stem must match the entry's id. Example:

yaml
# catalog/entries/use-cases/document-qa.yaml
id: document-qa
title: Document Q&A backed by a knowledge graph
kind: use-case
stack: use-case
tags:
  - document-qa
  - graph-rag
  - recall
  - reading-comprehension
summary: Answer questions grounded in a set of documents using cognee's graph-completion retrieval instead of plain RAG.
what_youll_build: A recall pipeline that ingests documents, extracts entities and relationships into a graph, and answers questions with citations back to source chunks.
quickstart: |
  git clone https://github.com/topoteretes/cognee.git
  cd cognee
  uv pip install cognee
  export LLM_API_KEY=your_openai_key
  python examples/guides/recall_core.py
expected_output: |
  A ranked list of retrieved passages plus a synthesized natural-language
  answer citing the original documents. Compare against a plain RAG baseline
  to see graph-completion's contribution.
difficulty: easy
example_path: examples/guides/recall_core.py

Fields

The full schema lives in catalog/schema.json. Highlights:

  • id: Lowercase, dashes, no spaces. Must match the filename stem. This is the stable machine identifier every downstream tool uses.
  • title: The human-readable name as it appears on the Hub card.
  • kind: integration, use-case, or package. Drives which subdirectory the file lives under and which view it appears in.
  • stack: The primary technology bucket (llm-provider, vector-store, graph-store, relational-store, framework, agent-runtime, workflow-tool, observability, loader, or use-case). Drives the Hub filter chips.
  • tags: Free-form filter labels. Redundant with stack on purpose so users can search by outcome or provider without hitting a taxonomy wall.
  • summary: One sentence, shown on the Hub card. Answers "does this work with my stack".
  • what_youll_build: One sentence promising a concrete outcome. Answers "what do I get if I follow the quickstart".
  • quickstart: A copy-paste block that gets a newcomer running. Include install, env, and a single run command. Multi-line YAML block scalar (quickstart: |).
  • expected_output: A concrete description of what the user should see after running the quickstart. Doesn't need to be exact values, but should be specific enough that a user knows if their run worked.
  • difficulty: easy, medium, or advanced. Measures the effort for a new user, not code complexity.
  • repo, path: Required for integrations and packages. Point at the source in one of topoteretes/cognee, topoteretes/cognee-community, or topoteretes/cognee-integrations.
  • example_path: Required for use-cases. Path within topoteretes/cognee to a runnable script.
  • inventory_slug: Optional. If your integration already has a slug in cognee-integrations/integrations/inventory.yml, set it here so the drift check keeps them aligned.
  • docs_url: Optional link to a longer doc page (e.g. docs.cognee.ai/integrations/openai).

Adding an entry: the workflow

  1. Pick the right subdirectory (integrations/, use-cases/, or packages/).
  2. Copy an existing entry as a starting point:
    bash
    cp catalog/entries/use-cases/document-qa.yaml catalog/entries/use-cases/your-new-entry.yaml
  3. Edit every field. The schema catches typos in field names, so a stray letter turns into a CI error, not a silent bug.
  4. Make sure the example_path (for use-cases) or the path (for integrations/packages pointing at topoteretes/cognee) resolves against the current checkout.
  5. If you're referencing an example that doesn't have a mocked test yet, coordinate with #3601 so the example runs in CI.
  6. Run the validation locally (a standard uv sync already provides the pyyaml and jsonschema the tooling needs):
    bash
    uv sync
    uv run python -m catalog.loader
    uv run python -m catalog.inventory_sync
    The loader must exit 0 for the entry to be considered valid โ€” that is the blocking gate. The drift check is advisory: it tolerates coverage gaps (integrations in the inventory but not yet in the catalog) and flags stale references, and it runs as a non-blocking CI step so an upstream change never fails your PR.
  7. Open a PR. The Catalog workflow will re-run both checks.

Coverage gaps

If you add an entry with an inventory_slug, the drift check confirms the slug exists upstream. If you don't add one, you're implicitly saying "this is a brand-new integration not in inventory.yml yet." That's fine.

If you want to help close a coverage gap, run the drift check locally to see the outstanding list, then add entries one at a time. Batch PRs of 5-10 entries are welcome; larger batches are harder to review.

What if I'm adding an integration that lives in a private fork?

Point repo at the private fork, or leave repo and path unset and treat it as a use-case (kind: use-case) with a public example_path. The catalog is designed for public discovery, so anything a user can't reach shouldn't be an entry.


How To Guides Cognee Sdk Deployment Coolify (`docs/how-to-guides/cognee-sdk/deployment/coolify.mdx`)


title: "Deploy Cognee on Coolify" icon: "anchor"

Coolify is an open-source, self-hostable PaaS โ€” a Heroku/Netlify
alternative that runs on your own server and deploys anything Docker can run. Because
Cognee ships a docker-compose.yml, Coolify can build, run, route, and TLS-terminate the
Cognee API for you, so you go from "it works on my laptop" to a public HTTPS endpoint without
writing any infrastructure code.

This guide deploys the Cognee API server (the cognee service). It uses a slim,
Coolify-friendly compose so the deployment is reliable on a modest server, and explains how to
add the optional services (databases, MCP server, UI) when you need them.

Architecture

mermaid
flowchart LR
    client(["API client / browser"]) -->|HTTPS| traefik

    subgraph server["Your server โ€” managed by Coolify"]
        traefik["Coolify proxy (Traefik)<br/>Let's Encrypt SSL"]
        cognee["cognee API<br/>:8000 ยท /health"]
        traefik --> cognee
        cognee -.->|"file-based, default"| vol[("SQLite ยท LanceDB ยท Ladybug<br/>persisted on a named volume")]
        cognee -.->|"optional"| ext[("Postgres ยท Neo4j ยท Redis")]
    end

By default Cognee uses file-based databases (SQLite + LanceDB + Ladybug, Cognee's embedded
graph engine), so a working deployment needs no external database โ€” just one API key.

Prerequisites

  • A server (VPS or bare metal) running a 64-bit Linux distro (Debian/Ubuntu recommended).
    Coolify itself idles at ~0.6โ€“1 GB RAM, so plan for 4 GB+ even though the documented
    minimum is 2 GB.
  • A running Coolify instance (v4). If you don't have one yet, follow Step 1 below first.
  • An LLM API key. Cognee defaults to OpenAI, so an OpenAI key works out of the box
    (LLM_API_KEY); any supported provider can be configured later.
  • (Optional) a domain name with a DNS A record pointing at your server, for a clean
    HTTPS URL. Coolify can also hand out a free sslip.io hostname.

Deployment at a glance

mermaid
flowchart TD
    A["Install Coolify on a Linux server"] --> B["Create New Resource โ†’ Public Repository"]
    B --> C["Build Pack: Docker Compose"]
    C --> D["Set environment variables<br/>(LLM_API_KEY, ENV=prod, โ€ฆ)"]
    D --> E["Assign a domain for HTTPS<br/>(SERVICE_FQDN_COGNEE_8000)"]
    E --> F["Deploy"]
    F --> G{"/health returns 200?"}
    G -->|Yes| H["Live: Cognee API over HTTPS"]
    G -->|No| I["Check Logs โ†’ Troubleshooting"]
    I --> F

Step 1 โ€” Install Coolify (skip if you already have it)

On a fresh server, run the official one-line installer as root (or with sudo):

bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash

Open the dashboard at http://<your-server-ip>:8000 and register the first account โ€” the
first user to register owns the instance, so do this immediately. Ports used by Coolify:
22 (SSH), 80 / 443 (proxy + SSL), and 8000 / 6001 / 6002 (dashboard, realtime,
terminal). Lock 8000 down to your own IP once you're set up.

Coolify's proxy needs ports **80/443**. If another reverse proxy or PaaS already owns them on the same host, free them first โ€” two proxies can't bind the same ports.

Step 2 โ€” Add Cognee as a Docker Compose resource

  1. Open (or create) a Project and pick an environment (e.g. production).
  2. Click Create New Resource and choose Public Repository.
  3. Paste the repository URL (the upstream repo or your fork):
    text
    https://github.com/topoteretes/cognee
  4. Coolify defaults the build pack to Nixpacks โ€” open that dropdown and select
    Docker Compose.
  5. Set Branch to main, Base Directory to /, and Docker Compose Location to the
    compose file you want to deploy (see below).

Use a slim, Coolify-friendly compose

Cognee's root docker-compose.yml is tuned for local development and has three rough edges
on Coolify. Knowing them saves hours:

Repo docker-compose.yml Why it bites on Coolify
Builds the image from source (build:) The multi-stage uv build is heavy and can OOM a small server. Coolify's own docs recommend a prebuilt image on low-RAM hosts.
Declares a custom network (cognee-network) Coolify warns that custom networks cause intermittent 504s; it auto-creates an isolated network per stack.
Gates optional services behind profiles: Coolify does not reliably honor compose profiles (issue #6395) โ€” it may start every service regardless.

The fix is a small, production-oriented compose that uses the official prebuilt image
(cognee/cognee:main), declares no custom network, has a single service (no profiles), injects
config via Coolify environment variables, and persists data on a named volume. (It also drops the
repo compose's published 5678 debugger port.) Commit this file to your fork and point
Docker Compose Location at it, or paste it via Coolify's raw compose editor:

yaml docker-compose.coolify.yml
services:
  cognee:
    image: cognee/cognee:main
    restart: always
    ports:
      - "8000:8000"
    environment:
      - LLM_API_KEY=${LLM_API_KEY}
      # Canonical env var (ENVIRONMENT is a deprecated alias). Use exactly "prod":
      # any other value enables FastAPI debug; "dev"/"local" also add Gunicorn auto-reload.
      - ENV=prod
      - DEBUG=false
      - LOG_LEVEL=INFO
      - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}
      # Keep the file-based databases on the persistent volume below.
      - DATA_ROOT_DIRECTORY=/app/.cognee/data
      - SYSTEM_ROOT_DIRECTORY=/app/.cognee/system
    volumes:
      - cognee_data:/app/.cognee
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

volumes:
  cognee_data:
The images `cognee/cognee` (API) and `cognee/cognee-mcp` (MCP server) are published to Docker Hub on every release, so the prebuilt image skips the source build entirely.

Reference: services in the full repo compose

If you do deploy the upstream docker-compose.yml, this is what it contains. Only services
without a profiles: key start under plain Docker Compose โ€” the cognee API (8000) and
redisinsight (5540):

Service Profile Default ports Purpose
cognee (none) 8000 Core API server
redisinsight (none) 5540 Redis GUI
cognee-mcp mcp 8001 MCP server for IDE integrations
frontend ui 3000 Experimental web UI
postgres postgres 5432 PostgreSQL + pgvector
neo4j neo4j 7474 / 7687 Neo4j graph database
redis redis 6379 Redis cache

Ports are host mappings; cognee-mcp listens on 8000 inside its container (mapped to host 8001).

These are Docker Compose **profiles** (activated with `--profile` / `COMPOSE_PROFILES` locally), **not** commented-out lines to uncomment. Because Coolify doesn't reliably honor profiles, the dependable way to choose services on Coolify is to put exactly what you want in the compose โ€” which is what the slim file above does.

Step 3 โ€” Configure environment variables

Open the resource's Environment Variables tab. Add variables one-by-one in Normal View,
or switch to Developer View to paste a block of KEY=VALUE lines at once. Cognee reads
variables straight from the container environment (they take precedence over any .env file),
so whatever you set here is applied on the next deploy.

The only variable you must set is the API key:

Variable Required Default Notes
LLM_API_KEY Yes โ€” OpenAI key by default; also used for embeddings if no separate key.
LLM_MODEL No openai/gpt-5-mini Override the model.
LLM_PROVIDER No openai openai, anthropic, gemini, ollama, โ€ฆ
CORS_ALLOWED_ORIGINS No * Lock down to your domain(s) in production.
The repo's `docker-compose.yml` **hard-codes `ENV=local`** inline on the `cognee` service. Inline compose values **override** anything you set in the UI, so adding `ENV=โ€ฆ` (or the deprecated `ENVIRONMENT=โ€ฆ`) there has **no effect** with that file. Use the slim compose above (it sets `ENV=prod`) or edit the service's `environment:` block. Use **exactly `ENV=prod`**: the app enables FastAPI debug mode for any other value (including `production`), and the entrypoint additionally turns on Gunicorn auto-reload + verbose logs when `ENV` is `dev` or `local`.

Step 4 โ€” Persist data and choose your databases

File-based (default, zero setup). Cognee defaults to SQLite (relational), LanceDB (vector)
and Ladybug (graph) โ€” Ladybug is Cognee's embedded graph engine; Kuzu is also supported. To
survive redeploys, keep them on a named volume โ€” Coolify persists
named volumes automatically (it appends the resource UUID to the name). The slim compose does
this with cognee_data plus DATA_ROOT_DIRECTORY / SYSTEM_ROOT_DIRECTORY.

External databases (optional). To move onto Postgres or Neo4j, set the matching provider
variables โ€” pointing only DB_HOST at a server is not enough. Run the database as a separate
Coolify resource, or add its service directly to your compose (declared normally, since Coolify
won't gate it behind a profile):

env
# PostgreSQL (relational) โ€” the bundled image is pgvector/pgvector:pg17
DB_PROVIDER=postgres
DB_HOST=postgres        # the compose service name, on the same Coolify network
DB_PORT=5432
DB_USERNAME=cognee
DB_PASSWORD=cognee
DB_NAME=cognee_db

# Vector store โ€” built-in options are lancedb (default) and pgvector
VECTOR_DB_PROVIDER=pgvector

# Graph store โ€” ladybug (default), kuzu, neo4j, โ€ฆ
GRAPH_DATABASE_PROVIDER=neo4j
GRAPH_DATABASE_URL=bolt://neo4j:7687
GRAPH_DATABASE_USERNAME=neo4j
GRAPH_DATABASE_PASSWORD=pleaseletmein

Step 5 โ€” Expose the API: domain, port & SSL

The cognee service listens on port 8000. To publish it through Coolify's proxy with
automatic HTTPS, assign a domain to the service โ€” either in its Domains field, or with
Coolify's magic variable in the compose:

env
SERVICE_FQDN_COGNEE_8000=https://cognee.example.com

The identifier is the service name (COGNEE) with the container port appended (_8000).
Entering an https:// domain makes Coolify request and auto-renew a Let's Encrypt
certificate via Traefik. (Magic variables in a Git-sourced compose require Coolify
v4.0.0-beta.411+; on older builds, assign the domain in the service's Domains field instead.)

Let's Encrypt needs the domain's DNS `A` record pointing at the server and ports **80/443** open, and it won't validate behind Cloudflare's proxied ("orange cloud") mode. Publishing a `ports:` mapping alone exposes plain HTTP on the host and **bypasses** the proxy/SSL โ€” use a domain assignment for managed TLS.

Step 6 โ€” Deploy and verify

Click Deploy and open the Logs. A healthy start runs the database migrations and then
Gunicorn binds the server:

text
Debug mode: false
Environment: prod
HTTP port: 8000
Bind address: 0.0.0.0
Running database migrations...
Database migrations done.
Starting server...

With ENV=prod, Gunicorn logs at error level, so the startup banner is quiet โ€” rely on the
health check below to confirm readiness.

Check the health endpoint โ€” a ready instance returns HTTP 200:

bash
curl https://cognee.example.com/health
json
{ "status": "ready", "health": "healthy", "version": "x.y.z" }

If a critical database or storage check fails, /health returns HTTP 503 with
{"status": "not ready", "health": "unhealthy", โ€ฆ} โ€” there is no separate "starting" state, and a
degraded-but-usable instance still returns 200. For a component-by-component breakdown call
GET /health/detailed, and the interactive API docs live at /docs.

With default settings Cognee runs in **multi-tenant mode** (`ENABLE_BACKEND_ACCESS_CONTROL=True`), so API endpoints require an authenticated user โ€” but `/health` and `/docs` stay open, which is all you need to confirm a successful deployment.

Production hardening

Before exposing Cognee publicly, review these defaults:

  • Authentication. ENABLE_BACKEND_ACCESS_CONTROL=True (default) requires auth on the API
    โ€” a REQUIRE_AUTHENTICATION=False is ignored while access control is on. Create a user, or for
    a single-user setup behind a token set ENABLE_BACKEND_ACCESS_CONTROL=False.
  • JWT secret. Set FASTAPI_USERS_JWT_SECRET to a securely generated secret, identical across
    replicas. When unset, each server process generates its own secret at startup, so tokens
    stop working after a restart and are not accepted by other replicas.
  • CORS. Replace the default * in CORS_ALLOWED_ORIGINS with your real front-end origin(s).
  • Secrets. Never commit API keys โ€” set them only as Coolify environment variables.

Troubleshooting

Symptom Likely cause Fix
Build is slow / server runs out of memory Building the image from source compiles many extras Deploy the prebuilt image (cognee/cognee:main) via the slim compose (Step 2)
Intermittent 504 Gateway Timeout A custom networks: block in the compose Remove custom networks; let Coolify manage the network (the slim compose has none)
Unwanted services (Postgres, Neo4jโ€ฆ) start anyway Coolify doesn't honor compose profiles: (#6395) Deploy a compose that contains only the services you want (the slim compose)
ENV / ENVIRONMENT changes have no effect Repo compose hard-codes ENV=local inline Edit the service environment:, or use the slim compose
Data disappears after a redeploy File-based DBs weren't on a persistent volume Use a named volume + DATA_ROOT_DIRECTORY / SYSTEM_ROOT_DIRECTORY (Step 2)
/health never turns ready Can't reach a configured external DB, or migrations failed Check Logs; verify DB_* / GRAPH_* values and that the DB is reachable
401/403 on API calls (not /health) Access control is on by default Authenticate, or set ENABLE_BACKEND_ACCESS_CONTROL=False for single-user mode
Let's Encrypt certificate fails DNS not pointing at server, ports 80/443 closed, or Cloudflare proxied Fix DNS/ports; set Cloudflare DNS to "DNS only"

Cost estimate

Cognee runs comfortably on a small VPS; the main variable cost is LLM API usage.

Tier Specs Approx. server cost
Minimal (file-based DBs, light use) 2 vCPU / 4 GB ~$5โ€“12 / month
Recommended (headroom for cognify) 4 vCPU / 8 GB ~$15โ€“30 / month
External DBs / heavier graphs 8 vCPU / 16 GB ~$30โ€“60 / month

LLM token costs are separate and depend on your provider, model, and how much data you ingest.

Next steps

  • Cognee SDK & API reference
  • Build your graph: add โ†’ cognify โ†’ search
  • Connect Cognee to your IDE by also deploying the MCP server (cognee/cognee-mcp)

--- METRICS ---