> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker Deployment

> Deploy Cognee and its supporting services using Docker Compose profiles

Deploy Cognee locally or on a server with Docker Compose. The included `docker-compose.yml` uses **profiles** so you can start only the services you need.

## Prerequisites

* [Docker](https://docs.docker.com/get-docker/) and Docker Compose v2+
* Git — only for the build-from-source path; the minimal Compose file below needs no clone

## Quick Start

Two ways to start the API server — a prebuilt image for a quick try-out, or the repository compose file when you want profiles, the UI, MCP, or external databases:

<Tabs>
  <Tab title="Minimal Compose (prebuilt image)">
    To try the API server without cloning or building, save this single file as `docker-compose.yml` in an empty directory. It runs the prebuilt [`cognee/cognee:main`](https://hub.docker.com/r/cognee/cognee) image with the default local databases (SQLite, LanceDB, Ladybug), so an LLM API key is the only thing you supply:

    ```yaml theme={null}
    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"
    ```

    Then start it:

    ```bash theme={null}
    export LLM_API_KEY="sk-..."   # your OpenAI API key
    docker compose up
    ```

    The `${LLM_API_KEY:?...}` guard is Compose variable interpolation: when `LLM_API_KEY` is unset, `docker compose up` aborts immediately and prints the message after `:?`, instead of starting a container that only fails later on the first LLM call.

    <Warning>
      `ENABLE_BACKEND_ACCESS_CONTROL: "false"` disables API authentication and per-user/dataset isolation so a first try-out needs no token. Use it for local experiments only — for anything shared or exposed, leave the flag at its `True` default and use the profile-based setup in the **Build from source** tab.
    </Warning>

    This file mounts nothing, so its data lives inside the container and is lost when the container is removed. See [Data Persistence and Host Files](#additional-information) for a named-volume variant. For other LLM providers, add the matching `LLM_PROVIDER` / `LLM_MODEL` / `LLM_ENDPOINT` variables — the repository `.env.template` lists them all.
  </Tab>

  <Tab title="Build from source">
    ```bash theme={null}
    git clone https://github.com/topoteretes/cognee.git
    cd cognee
    cp .env.template .env
    ```

    Edit `.env` and set your LLM API key:

    ```bash theme={null}
    LLM_API_KEY="your_api_key"
    ```

    Then start the Cognee API server (no profile needed):

    ```bash theme={null}
    docker compose up --build cognee
    ```
  </Tab>
</Tabs>

Either way, the API will be available at `http://localhost:8000`. Interactive docs at `http://localhost:8000/docs`.

## Verify Deployment

After the server starts, check that the API process is reachable:

```bash theme={null}
curl -f http://localhost:8000/health
```

This only proves that the server is alive. It does **not** prove that ingestion, graph building, vector search, or LLM-backed recall works.

### Container Health Status

The `cognee` and `cognee-mcp` images declare a Docker `HEALTHCHECK`, so Docker polls `/health` for you and tracks the result as container state. `docker ps` shows `(health: starting)`, `(healthy)`, or `(unhealthy)` in the `STATUS` column, and you can read the current state directly:

```bash theme={null}
docker inspect --format '{{.State.Health.Status}}' "$(docker compose ps -q cognee)"
```

| Image               | Probe                                                                   | Interval | Timeout | Start period | Retries |
| ------------------- | ----------------------------------------------------------------------- | -------- | ------- | ------------ | ------- |
| `cognee/cognee`     | `curl -f http://localhost:8000/health`                                  | `30s`    | `10s`   | `40s`        | `3`     |
| `cognee/cognee-mcp` | `GET http://localhost:8000/health`, skipped when `TRANSPORT_MODE=stdio` | `30s`    | `10s`   | `60s`        | `3`     |

In the MCP server's default `stdio` transport there is no HTTP server to probe, so the check reports healthy without touching the network. It only makes a real request under the HTTP/SSE transports.

Both probes target port `8000` **inside** the container, which is where the entrypoint binds by default — including the `mcp` profile, where `8001` is only the published host port. If you change `HTTP_PORT`, the baked-in healthcheck no longer matches the listening port; override `healthcheck.test` for that service in your compose file.

Because the health state is part of the image, other services can wait on Cognee the same way the [Postgres and Neo4j examples](#permissionerror-external-databases) wait on their databases:

```yaml theme={null}
services:
  my-service:
    depends_on:
      cognee:
        condition: service_healthy
```

<Note>
  This metadata is baked in at build time. Images published before the `HEALTHCHECK` was added carry no health state at all: `docker ps` shows a plain `Up` status with no health annotation, and the `docker inspect` command above has nothing to report. If `condition: service_healthy` never becomes satisfiable, pull a newer tag or rebuild locally with `docker compose up --build cognee`.
</Note>

### Image Provenance and SBOM

Images built by the release pipeline carry in-toto provenance and SBOM attestations, pushed alongside the image manifest. To confirm an image was built by CI from `topoteretes/cognee` and to inspect its bill of materials:

```bash theme={null}
# Provenance attestation
docker buildx imagetools inspect cognee/cognee:latest --format '{{ json .Provenance }}'

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

The same commands work for the MCP image (`cognee/cognee-mcp`). Like the healthcheck metadata above, attestations are attached at build time — images published before the release pipeline added them (August 2026) have nothing to report. For the full mechanism, see [Supply-chain provenance & release attestations](https://github.com/topoteretes/cognee/blob/dev/docs/supply_chain_provenance.md) in the cognee repo.

## Smoke Test Ingestion and Recall

Docker users often test API routes immediately after startup. Cognee API endpoints use the versioned `/api/v1` prefix, not plain `/api`; see [API Base URLs](/api-reference/introduction#api-base-urls) for the full API reference note.

By default, `ENABLE_BACKEND_ACCESS_CONTROL=True` makes API authentication required. For a local unauthenticated smoke test, set `ENABLE_BACKEND_ACCESS_CONTROL=false` in `.env` and restart the container, or include a valid Bearer token in the `curl` requests.

Create a small file, ingest it synchronously, then query the same dataset:

```bash theme={null}
printf "Cognee turns data into searchable AI memory." > /tmp/cognee-smoke.txt

curl -X POST http://localhost:8000/api/v1/remember \
  -F "data=@/tmp/cognee-smoke.txt" \
  -F "datasetName=smoke_test" \
  -F "run_in_background=false"

curl -X POST http://localhost:8000/api/v1/recall \
  -H "Content-Type: application/json" \
  -d '{"query": "What does Cognee do?", "datasets": ["smoke_test"], "search_type": "GRAPH_COMPLETION", "top_k": 5}'
```

On the minimal Compose stack above, `ENABLE_BACKEND_ACCESS_CONTROL` is already `false`, so these calls work unauthenticated with no further changes. If you prefer the explicit three-step flow over `remember`/`recall`, the same result comes from `add` → `cognify` → `search`:

```bash theme={null}
echo "Cognee turns documents into AI memory." > note.txt

# Ingest a file — /api/v1/add takes a multipart upload, it does not accept inline text
curl -X POST http://localhost:8000/api/v1/add \
  -F "data=@note.txt" \
  -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"]}'
```

## Additional Information

<AccordionGroup>
  <Accordion title="Docker Compose Services">
    Each optional service is gated behind a profile. Use `--profile` to activate one or more:

    | Profile    | Service        | Port(s)        | Purpose                                                                                |
    | ---------- | -------------- | -------------- | -------------------------------------------------------------------------------------- |
    | *(none)*   | `cognee`       | `8000`, `5678` | Core API server                                                                        |
    | *(none)*   | `redisinsight` | `5540`         | RedisInsight GUI for inspecting Redis; developer convenience only                      |
    | `mcp`      | `cognee-mcp`   | `8001`, `5679` | MCP server for IDE integrations (host ports; container still listens on `8000`/`5678`) |
    | `ui`       | `frontend`     | `3000`         | Experimental web UI                                                                    |
    | `neo4j`    | `neo4j`        | `7474`, `7687` | Neo4j graph database                                                                   |
    | `postgres` | `postgres`     | `5432`         | PostgreSQL + pgvector                                                                  |
    | `redis`    | `redis`        | `6379`         | Redis session cache                                                                    |

    Services with no profile start on a bare `docker compose up`, so `redisinsight` comes up alongside `cognee` unless you name the services you want (`docker compose up cognee`).

    For what each service does, whether you need it, and how `cognee-network`, `extra_hosts`, and the resource limits work, see the [Docker Compose Reference](/how-to-guides/cognee-sdk/deployment/docker-compose-reference).
  </Accordion>

  <Accordion title="Data Persistence and Host Files">
    Both images store their data **outside the source tree**, under `/cognee-storage`. The `Dockerfile` and `cognee-mcp/Dockerfile` bake in these defaults:

    ```dockerfile theme={null}
    ENV SYSTEM_ROOT_DIRECTORY=/cognee-storage/system
    ENV DATA_ROOT_DIRECTORY=/cognee-storage/data
    ```

    The compose file mounts the `cognee_system` and `cognee_data` named volumes at exactly those paths, on **both** the `cognee` and `cognee-mcp` services, so the API server and the MCP server share one memory store and it survives container recreation. The `./cognee` bind mount is for dev reload only — it is no longer where data is persisted — and `.env` is mounted read-only (`:ro`).

    The database services each map to a distinct role, but only services with an active `volumes:` entry in `docker-compose.yml` persist data through container recreation by default:

    | Storage area         | Role                                                                                                                                                         | Persistence in the checked-in compose file                                                       |
    | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
    | `redis`              | [Session/conversation cache](/core-concepts/sessions-and-caching)                                                                                            | Uses the mounted `redis_data` named volume                                                       |
    | `postgres`           | Relational metadata/state ([SQLite](/setup-configuration/relational-databases) by default, or Postgres), and vector store when `VECTOR_DB_PROVIDER=pgvector` | Uses the mounted `postgres_data` named volume                                                    |
    | Embedded graph store | [Knowledge graph](/setup-configuration/graph-stores) files under `SYSTEM_ROOT_DIRECTORY` (`/cognee-storage/system` in both images)                           | Uses the mounted `cognee_system` named volume, shared with `cognee-mcp`                          |
    | Ingestion artifacts  | Uploaded files, loader outputs, and caches under `DATA_ROOT_DIRECTORY` (`/cognee-storage/data` in both images)                                               | Uses the mounted `cognee_data` named volume, shared with `cognee-mcp`                            |
    | `neo4j`              | Dedicated graph database when `GRAPH_DATABASE_PROVIDER=neo4j`                                                                                                | Runs in its own service; add a Neo4j data volume for durability across container recreation      |
    | ChromaDB             | [Vector store](/setup-configuration/vector-stores) for embeddings when `VECTOR_DB_PROVIDER=chromadb`                                                         | Not in the shipped compose file; persist the Chroma data directory in the Chroma service you add |

    If `GRAPH_DATABASE_PROVIDER` is unset, the application default graph provider is **Ladybug**. The repository `.env.template` currently sets **Kuzu** for Docker. Both are embedded file-based graph stores, so the graph files live under `SYSTEM_ROOT_DIRECTORY` unless you switch to a dedicated graph service.

    The shipped compose file is therefore already persistent for the embedded stores. If you prefer an external graph database, run Neo4j with `--profile neo4j` and set `GRAPH_DATABASE_PROVIDER=neo4j`. See [Cognee + PostgreSQL + Neo4j](#postgresql-neo4j) and [PermissionError with External Databases](#permissionerror-external-databases) for volume examples.

    The [minimal Compose file](#quick-start) mounts nothing, so an image-only run keeps everything inside the container and loses it on `docker compose down`. To keep data across container recreation, extend that file with named volumes at the image's default storage roots — no environment variables needed, since the image already points there:

    ```yaml theme={null}
    services:
      cognee:
        # ...minimal file from Quick Start, plus:
        volumes:
          - cognee_system:/cognee-storage/system
          - cognee_data:/cognee-storage/data

    volumes:
      cognee_system:
      cognee_data:
    ```

    <Note>
      A fresh named volume mounted at `/cognee-storage/system` or `/cognee-storage/data` inherits the ownership Docker finds baked into the image at that path — `cognee:cognee` (uid/gid 1000), the user the container runs as — so it initializes writable with no `chown` on your part. A **host bind mount** does not: Docker uses the host directory's existing ownership, so `chown 1000:1000` it before starting the container. See [PermissionError with External Databases](#permissionerror-external-databases).
    </Note>

    To ingest files from your host machine, uncomment and update the volume in `docker-compose.yml`.

    ```yaml theme={null}
    # - /path/to/your/data:/data
    ```
  </Accordion>

  <Accordion title="Docker Environment Variables">
    The `cognee` container reads configuration from `.env` at startup. Key variables:

    | Variable                        | Default                                                  | Description                                                                                                                                                                                                                          |
    | ------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `LLM_API_KEY`                   | *(required)*                                             | API key for your LLM provider                                                                                                                                                                                                        |
    | `LLM_MODEL`                     | `openai/gpt-5-mini`                                      | LLM model to use                                                                                                                                                                                                                     |
    | `DB_PROVIDER`                   | `sqlite`                                                 | Relational DB: `sqlite` or `postgres`                                                                                                                                                                                                |
    | `GRAPH_DATABASE_PROVIDER`       | `kuzu` in `.env.template`                                | Graph DB: `kuzu`, `neo4j`, etc. If unset, the application default is `ladybug`.                                                                                                                                                      |
    | `VECTOR_DB_PROVIDER`            | `lancedb`                                                | Vector DB: `lancedb`, `chromadb`, `pgvector`, etc.                                                                                                                                                                                   |
    | `SYSTEM_ROOT_DIRECTORY`         | `/cognee-storage/system` (baked into both images)        | Embedded graph/vector files and other system state. Mount a volume here to persist it                                                                                                                                                |
    | `DATA_ROOT_DIRECTORY`           | `/cognee-storage/data` (baked into both images)          | Ingested files, loader outputs, and caches. Mount a volume here to persist it                                                                                                                                                        |
    | `CORS_ALLOWED_ORIGINS`          | `*` in Docker Compose                                    | Restrict to specific domains in production                                                                                                                                                                                           |
    | `HTTP_PORT`                     | `8000`                                                   | Port the API server binds inside the container (entrypoint default)                                                                                                                                                                  |
    | `BIND_ADDRESS`                  | `0.0.0.0`                                                | Address the API server binds inside the container (entrypoint default)                                                                                                                                                               |
    | `ENABLE_BACKEND_ACCESS_CONTROL` | `True`                                                   | Enables per-user/dataset isolation. When this is `True`, authentication is required.                                                                                                                                                 |
    | `REQUIRE_AUTHENTICATION`        | Inherits from `ENABLE_BACKEND_ACCESS_CONTROL` when unset | Enable JWT auth for the API. Setting this to `False` is ignored when `ENABLE_BACKEND_ACCESS_CONTROL=True`.                                                                                                                           |
    | `COGNEE_SKIP_CONNECTION_TEST`   | `false`                                                  | Skip LLM/embedding connectivity checks on startup, and the [zero-network provider-consistency check](/setup-configuration/overview#configuration-workflow) that `add()` and `remember()` run. Accepts `true`, `1`, or `yes`.         |
    | `ENABLE_AUTO_MIGRATIONS`        | `true`                                                   | Run database migrations automatically, including at container startup. Set to `false` (or `0`/`no`) to migrate explicitly with `cognee-cli upgrade` instead — see [Database Migrations on Startup](#database-migrations-on-startup). |
    | `DEBUG`                         | `false`                                                  | When `true` and `ENV` is `dev` or `local`, the container entrypoint starts under `debugpy` listening on `DEBUG_PORT`                                                                                                                 |
    | `DEBUG_PORT`                    | `5678`                                                   | Port `debugpy` listens on when `DEBUG=true`                                                                                                                                                                                          |
    | `chunk_size`                    | `1500`                                                   | Max tokens per chunk during cognify (see [Chunkers](/core-concepts/further-concepts/chunkers))                                                                                                                                       |
    | `chunk_overlap`                 | `10`                                                     | Overlap between chunks in words (only affects `LangchainChunker`)                                                                                                                                                                    |

    `ENVIRONMENT` is a deprecated alias for `ENV`, still accepted by the container entrypoints — prefer `ENV`.

    See the full list of options in [Setup Configuration](/setup-configuration/overview).
  </Accordion>

  <Accordion title="Database Migrations on Startup" id="database-migrations-on-startup">
    Before the API server binds its port, the container entrypoint runs Cognee's own startup migrations — the same [`run_migrations()`](/python-api/run-migrations) path used by the API server's lifespan and by `cognee-cli`. You will see this in the container logs:

    ```
    Running database migrations...
    Database migrations done.
    Starting server...
    ```

    What runs depends on the state of the database it finds:

    * **Fresh volume / empty database** — Cognee creates the missing directories, builds the schema from its models, and stamps `alembic_version` at head instead of replaying the whole revision history. A database is only treated as empty when it has neither a `users` table nor an `alembic_version` table, so a pre-Alembic legacy database is migrated rather than wrongly stamped.
    * **Existing database** — Alembic applies the pending relational revisions, then the graph/vector data migration chain runs.

    A failed **relational** migration aborts the boot with a non-zero exit rather than starting the server on an unmigrated schema. A per-dataset **data-chain** failure does not stop the boot: the server comes up, Cognee blocks writes to just those datasets, and the migration is retried on the next start. The entrypoint prints the affected datasets:

    ```
    Data migrations failed for: <dataset ids>. Writes to those datasets are blocked until they migrate; retried on the next start.
    ```

    Set `ENABLE_AUTO_MIGRATIONS=false` to turn off this automatic run and migrate explicitly instead — `cognee-cli upgrade` ignores the flag and always migrates. See [Troubleshooting → Migration Fails on First Boot](#migration-fails-first-boot) if the container exits during this step.
  </Accordion>

  <Accordion title="Common setups">
    <AccordionGroup>
      <Accordion title="Cognee + PostgreSQL">
        PostgreSQL with pgvector is a good production choice for the relational database.

        Add to your `.env`:

        ```bash theme={null}
        DB_PROVIDER=postgres
        DB_HOST=postgres
        DB_PORT=5432
        DB_USERNAME=cognee
        DB_PASSWORD=cognee
        DB_NAME=cognee_db
        ```

        Start both services:

        ```bash theme={null}
        docker compose --profile postgres up --build
        ```
      </Accordion>

      <Accordion title="Cognee + PostgreSQL + Neo4j" id="postgresql-neo4j">
        For production deployments with a dedicated graph database:

        Add to your `.env`:

        ```bash theme={null}
        # Relational DB
        DB_PROVIDER=postgres
        DB_HOST=postgres
        DB_PORT=5432
        DB_USERNAME=cognee
        DB_PASSWORD=cognee
        DB_NAME=cognee_db

        # Graph DB
        GRAPH_DATABASE_PROVIDER=neo4j
        GRAPH_DATABASE_URL=bolt://neo4j:7687
        GRAPH_DATABASE_NAME=neo4j
        GRAPH_DATABASE_USERNAME=neo4j
        GRAPH_DATABASE_PASSWORD=pleaseletmein
        ```

        The shipped `postgres` service already mounts the `postgres_data` volume. Neo4j does not, so add one for graph durability across container recreation:

        ```yaml theme={null}
        services:
          neo4j:
            volumes:
              - neo4j_data:/data

        volumes:
          neo4j_data:
        ```

        Start the stack:

        ```bash theme={null}
        docker compose --profile postgres --profile neo4j up --build
        ```

        Neo4j browser is available at `http://localhost:7474`.
      </Accordion>

      <Accordion title="Cognee + ChromaDB">
        Use ChromaDB as the vector store. The shipped `docker-compose.yml` has no `chromadb` service, so add one yourself:

        ```yaml theme={null}
        services:
          chromadb:
            image: chromadb/chroma:latest
            profiles:
              - chromadb
            ports:
              - 8002:8000
            networks:
              - cognee-network
        ```

        Add to your `.env`:

        ```bash theme={null}
        VECTOR_DB_PROVIDER=chromadb
        VECTOR_DB_URL=http://chromadb:8000
        VECTOR_DB_KEY=your_chroma_token
        ```

        Start:

        ```bash theme={null}
        docker compose --profile chromadb up --build
        ```
      </Accordion>

      <Accordion title="Cognee + MCP Server">
        Run the [MCP server](/cognee-mcp/mcp-overview) alongside the API:

        ```bash theme={null}
        docker compose --profile mcp up --build cognee-mcp
        ```

        The MCP server uses SSE transport and is published on host port `8001` (the container itself still listens on `8000`, so the `mcp` profile doesn't collide with the `cognee` API service when both run). Configure your IDE to point to `http://localhost:8001/sse`. The debugger is published on host port `5679`.
      </Accordion>

      <Accordion title="Cognee + Web UI">
        The `ui` profile starts the same web interface that [`cognee.start_ui()`](/cognee-cloud/local-ui) launches locally — here it runs as a separate `frontend` container:

        ```bash theme={null}
        docker compose --profile ui up --build
        ```

        The backend API and the UI listen on **different ports**, so they don't conflict:

        | Service                | URL                     | Port   |
        | ---------------------- | ----------------------- | ------ |
        | API backend (`cognee`) | `http://localhost:8000` | `8000` |
        | Web UI (`frontend`)    | `http://localhost:3000` | `3000` |

        By default the frontend's local API client targets port `8000` on whatever host you loaded the UI from, so browsing to the UI on `localhost` or `127.0.0.1` reaches the API on that same host. Keep the API published on port `8000` for the default Compose setup. If your API is reachable at a different host or port, pass `NEXT_PUBLIC_LOCAL_API_URL` to the `frontend` container with that backend URL.

        <Note>
          Don't also call `cognee.start_ui()` while the `ui` profile is running — both bind port `3000`, so the second will fail with a "port already in use" error. In a Docker deployment use the `ui` profile; reserve [`cognee.start_ui()`](/cognee-cloud/local-ui) for non-Docker, local Python setups.
        </Note>
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Managing the Docker Deployment">
    The `cognee` container reads `.env` **once at startup**, so edits to `.env` are not picked up by a running container. Restart the service to apply them:

    ```bash theme={null}
    # Re-reads .env and restarts the cognee process
    docker compose restart cognee
    ```

    If you changed the `docker-compose.yml` definition itself (ports, volumes, `environment:`, profiles), recreate the container instead so the new settings take effect:

    ```bash theme={null}
    docker compose up -d --force-recreate cognee
    ```

    You only need `--build` when you change the `Dockerfile`, its dependencies, or the build arguments passed to it (for example, [adding optional extras](#additional-information) via `COGNEE_EXTRAS`) — not for `.env` edits:

    ```bash theme={null}
    docker compose up --build cognee
    ```

    <Note>
      Your `.env` and the `cognee/` source directory are bind-mounted into the container, so a restart is enough to apply config changes — no rebuild required.
    </Note>

    Stop or remove containers with Docker Compose:

    ```bash theme={null}
    # Stop containers (preserves volumes)
    docker compose down

    # Stop and remove volumes (deletes all data)
    docker compose down --volumes
    ```
  </Accordion>

  <Accordion title="Optional Extras and Document Loaders">
    The default Docker image includes a fixed set of extras from the repository `Dockerfile`: `fastembed`, `debug`, `api`, `postgres`, `neo4j`, `llama-index`, `aws`, `ollama`, `mistral`, `groq`, and `anthropic`. In particular, the `aws` extra (s3fs/boto3 for [S3 file storage](/guides/s3-storage)) is part of the defaults, so it does not need to be added at build time. The `fastembed` extra (`fastembed` plus a compatible `onnxruntime`) is also included, so [local CPU embeddings](/setup-configuration/embedding-providers#fastembed-local) work in the image without a custom build. To install additional optional dependencies, pass the `COGNEE_EXTRAS` build argument — a space-separated list of extra names, added on top of the defaults. No `Dockerfile` edit is required:

    ```bash theme={null}
    docker build --build-arg COGNEE_EXTRAS="docs langchain" -t cognee-custom .
    ```

    `COGNEE_EXTRAS` defaults to an empty string, so builds that don't pass it behave exactly as before. The `Dockerfile` applies it to **both** `uv sync` steps — the second sync is exact and would otherwise remove extras installed only in the dependency-cache layer — so the packages end up in the final runtime stage.

    <Note>
      Both `uv sync` invocations keep `--frozen`. `COGNEE_EXTRAS` selects from extras that are already resolved in `uv.lock`; it does not resolve new dependencies. Builds stay deterministic and no lockfile change is needed.
    </Note>

    The build argument is declared in the root `Dockerfile`, which builds the API image. The MCP and frontend images use their own Dockerfiles and do not accept it.

    **Passing extras through Docker Compose.** The `cognee` service's `build:` block in `docker-compose.yml` declares only `context` and `dockerfile`, so `docker compose up --build cognee` does *not* forward `COGNEE_EXTRAS`. Add an `args:` entry first:

    ```yaml theme={null}
    services:
      cognee:
        build:
          context: .
          dockerfile: Dockerfile
          args:
            COGNEE_EXTRAS: "docs scraping"
    ```

    Then rebuild:

    ```bash theme={null}
    docker compose up --build cognee
    ```

    For a table of available extras and common combinations, see [Installation](/getting-started/installation#extras-and-common-installation-combinations).
    For a table of supported file types and their loaders, see [Loaders](/core-concepts/further-concepts/loaders#supported-file-extensions).

    For example, the `docs` extra adds [UnstructuredLoader](/core-concepts/further-concepts/loaders#external-loaders), office documents (`.docx`, `.pptx`, `.xlsx`, `.epub`, and similar formats), and `AdvancedPdfLoader`. Other commonly added extras include `scraping`, `redis`, `tracing`, and `docling`.

    **System packages still require a `Dockerfile` edit.** `COGNEE_EXTRAS` only installs Python packages. For layout-aware or OCR-based PDF extraction with `AdvancedPdfLoader`, you also need `poppler-utils` and `tesseract-ocr` in the **runtime stage** of your `Dockerfile` (the second `FROM python:3.12-slim-bookworm` block):

    ```dockerfile theme={null}
    RUN apt-get update && apt-get install -y \
        libpq5 \
        curl \
        poppler-utils \
        tesseract-ocr \
        && rm -rf /var/lib/apt/lists/*
    ```

    Rebuild after updating the `Dockerfile`:

    ```bash theme={null}
    docker compose up --build cognee
    ```
  </Accordion>

  <Accordion title="Bytecode Precompilation">
    The repository `Dockerfile` sets `ENV UV_COMPILE_BYTECODE=1`, so `uv sync` compiles the virtual environment to `.pyc` bytecode at **build time** instead of leaving the interpreter to recompile each module from source on first import.

    The effect is faster container cold starts: without it the shipped venv contains no `.pyc` files, so every cold start recompiles the dependency tree from source. On the `cognee-saas-pod` image this accounted for roughly 8s of a \~13s import — about half the startup time.

    Trade-offs: builds take slightly longer and the image is marginally larger because the `.pyc` files are written into the venv layer.

    To disable it (for example to debug or reproduce from-source import behavior), comment out or remove the line in the `Dockerfile` before building:

    ```dockerfile theme={null}
    # ENV UV_COMPILE_BYTECODE=1
    ```

    then rebuild:

    ```bash theme={null}
    docker compose up --build cognee
    ```
  </Accordion>

  <Accordion title="Troubleshooting">
    <AccordionGroup>
      <Accordion title="PermissionError with External Databases" id="permissionerror-external-databases">
        Even when Cognee is configured to use external databases (Postgres, pgvector, Neo4j, etc.), local writable paths are **still required**. `DATA_ROOT_DIRECTORY` (SDK default `.data_storage`) and `SYSTEM_ROOT_DIRECTORY` (SDK default `.cognee_system`) hold ingestion artifacts, file caches, and loader outputs — they are not bypassed by pointing the relational, vector, or graph backends elsewhere.

        The `cognee/cognee` and `cognee/cognee-mcp` images override those defaults to `/cognee-storage/data` and `/cognee-storage/system`, and run as the non-root user `cognee` (**uid/gid 1000**). If the mounted path is read-only or owned by another user, ingestion fails with:

        ```
        PermissionError: [Errno 13] Permission denied: '/cognee-storage/data/...'
        ```

        The usual cause is a **host bind mount**: named volumes inherit the image's `cognee:cognee` ownership, but a bind-mounted host directory keeps the host's ownership, which is rarely uid 1000. Fix it on the host before starting the container:

        ```bash theme={null}
        sudo chown -R 1000:1000 ./my-cognee-storage
        ```

        **Fix — mount writable volumes at the image's storage roots**:

        ```yaml theme={null}
        services:
          cognee:
            image: cognee/cognee:main
            volumes:
              - cognee_data:/cognee-storage/data
              - cognee_system:/cognee-storage/system
            environment:
              DB_PROVIDER: postgres
              # ... remaining DB / graph / vector settings

        volumes:
          cognee_data:
          cognee_system:
        ```

        If you relocate the storage paths with `DATA_ROOT_DIRECTORY` and `SYSTEM_ROOT_DIRECTORY`, mount the volumes at the same paths:

        ```yaml theme={null}
        services:
          cognee:
            image: cognee/cognee:main
            volumes:
              - cognee_data:/var/cognee/data
              - cognee_system:/var/cognee/system
            environment:
              DATA_ROOT_DIRECTORY: /var/cognee/data
              SYSTEM_ROOT_DIRECTORY: /var/cognee/system
              DB_PROVIDER: postgres
              # ... remaining DB / graph / vector settings

        volumes:
          cognee_data:
          cognee_system:
        ```

        **Working Postgres + pgvector + Neo4j compose example** — includes healthchecks on both `postgres` and `neo4j` so Cognee does not start before either database is ready (Cognee otherwise races Neo4j's Bolt listener and exits with a connection error):

        ```yaml theme={null}
        services:
          postgres:
            image: pgvector/pgvector:pg17
            environment:
              POSTGRES_USER: cognee
              POSTGRES_PASSWORD: cognee
              POSTGRES_DB: cognee_db
            healthcheck:
              test: ["CMD-SHELL", "pg_isready -U cognee -d cognee_db"]
              interval: 10s
              timeout: 5s
              retries: 5

          neo4j:
            image: neo4j:5.26
            environment:
              NEO4J_AUTH: neo4j/pleaseletmein
            healthcheck:
              test: ["CMD-SHELL", "cypher-shell -u neo4j -p pleaseletmein 'RETURN 1'"]
              interval: 10s
              timeout: 5s
              retries: 10
              start_period: 30s

          cognee:
            image: cognee/cognee:main
            depends_on:
              postgres:
                condition: service_healthy
              neo4j:
                condition: service_healthy
            volumes:
              - cognee_data:/cognee-storage/data
              - cognee_system:/cognee-storage/system
            environment:
              DB_PROVIDER: postgres
              DB_HOST: postgres
              DB_PORT: 5432
              DB_USERNAME: cognee
              DB_PASSWORD: cognee
              DB_NAME: cognee_db
              VECTOR_DB_PROVIDER: pgvector
              GRAPH_DATABASE_PROVIDER: neo4j
              GRAPH_DATABASE_URL: bolt://neo4j:7687
              GRAPH_DATABASE_USERNAME: neo4j
              GRAPH_DATABASE_PASSWORD: pleaseletmein

        volumes:
          cognee_data:
          cognee_system:
        ```

        See [Storage & Logging](/setup-configuration/overview#storage-amp-logging) for the related env vars, or [S3 storage](/guides/s3-storage) if you want to point these directories at S3 instead of local volumes.
      </Accordion>

      <Accordion title="PostgreSQL Connection Refused" id="postgresql-connection-refused">
        When Cognee starts before PostgreSQL finishes initializing, the first API call triggers LLM/embedding connectivity checks (`setup_and_check_environment`) and may hit the database before it accepts connections, producing `[Errno 111] Connection refused` or `[Errno 99] Cannot assign requested address`.

        **Recommended fix — add a healthcheck and `depends_on` condition to your `docker-compose.yml`.** The shipped compose file already carries this exact `pg_isready` healthcheck on the `postgres` service, so with it you only need to add the `depends_on` guard; the full example below is for hand-written compose files:

        ```yaml theme={null}
        services:
          postgres:
            image: pgvector/pgvector:pg17
            environment:
              POSTGRES_USER: cognee
              POSTGRES_PASSWORD: cognee
              POSTGRES_DB: cognee_db
            healthcheck:
              test: ["CMD-SHELL", "pg_isready -U cognee -d cognee_db"]
              interval: 10s
              timeout: 5s
              retries: 5

          cognee:
            image: cognee/cognee:main
            depends_on:
              postgres:
                condition: service_healthy
            environment:
              DB_PROVIDER: postgres
              DB_HOST: postgres
              DB_PORT: 5432
              DB_USERNAME: cognee
              DB_PASSWORD: cognee
              DB_NAME: cognee_db
        ```

        This delays the `cognee` container until PostgreSQL passes its health check.

        **Alternative fix — bypass the connectivity check:**

        If you cannot modify the compose file (e.g. third-party orchestration), set `COGNEE_SKIP_CONNECTION_TEST=true` to skip the LLM/embedding startup probe entirely. The check is only performed once (on first run), so the trade-off is that misconfigured endpoints are not caught until the first real request.

        ```bash theme={null}
        COGNEE_SKIP_CONNECTION_TEST=true
        ```
      </Accordion>

      <Accordion title="Migration Fails on First Boot" id="migration-fails-first-boot">
        The entrypoint runs [startup migrations](#database-migrations-on-startup) before the server starts, and a failed relational migration exits non-zero — so the container stops right after `Running database migrations...` instead of reaching `Starting server...`. Two causes account for most first-boot failures:

        * **The storage directories are not writable.** The relational database (SQLite by default) and its parent directory live under `DATA_ROOT_DIRECTORY` / `SYSTEM_ROOT_DIRECTORY`. If those paths are read-only or owned by another user, the migration cannot create or open the database file. Mount writable volumes for both, as shown in [PermissionError with External Databases](#permissionerror-external-databases).
        * **An external database is not reachable yet.** With `DB_PROVIDER=postgres`, the migration runs before the server would otherwise touch the database, so a Postgres container that is still initializing fails the boot. Add a `depends_on: condition: service_healthy` guard (the shipped compose file already has the healthcheck), as in [PostgreSQL Connection Refused](#postgresql-connection-refused).

        Both are safe to retry: restart the container once the volume or database is ready and the migration runs again from where it left off.

        **Operator-driven alternative** — if you would rather migrate outside container startup (for example, from a one-shot job that runs before the app rolls out), disable the automatic run and invoke the CLI yourself:

        ```yaml theme={null}
        services:
          cognee:
            image: cognee/cognee:main
            environment:
              ENABLE_AUTO_MIGRATIONS: "false"
        ```

        ```bash theme={null}
        # Run once, before starting the app container
        docker compose run --rm cognee cognee-cli upgrade
        ```

        `cognee-cli upgrade` ignores `ENABLE_AUTO_MIGRATIONS` and always migrates. Leaving migrations disabled without running it means the schema is never brought to head — the server starts against whatever schema exists.
      </Accordion>

      <Accordion title="Web UI Login Loops Back to the Login Page" id="ui-login-loop">
        On the `ui` profile, signing in returns `200` but the app immediately bounces back to `/local-login`, repeating on every attempt.

        The auth cookie is host-scoped: it carries no `Domain` attribute, so a cookie set on `localhost` is not sent to `127.0.0.1` and vice versa. Older frontend builds always sent local API requests to a hard-coded `http://localhost:8000`, so opening the UI on `http://127.0.0.1:3000` stored the cookie for `localhost` while the page ran on `127.0.0.1`. The follow-up `GET /api/v1/users/me` check went out without the cookie, returned `401`, and the UI redirected back to the login page.

        Current builds resolve the API host from the page you loaded, so `localhost` and `127.0.0.1` both work — the shipped compose file allows every origin (`CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}`), so no extra configuration is needed. If you still see the loop:

        * **Update your checkout.** The `frontend` service builds from `./cognee-frontend` and bind-mounts `src`, so it runs whatever source your clone has. Pull the latest and restart it: `docker compose --profile ui up -d --build frontend`.
        * **Check any `NEXT_PUBLIC_LOCAL_API_URL` you set.** An explicit value always wins over the browser-derived host, so it reintroduces the mismatch if its hostname differs from the one in your address bar.
        * **Check any narrowed `CORS_ALLOWED_ORIGINS`.** If you replaced the default `*`, it must name the exact origin you browse to, port included.
        * **Clear cookies** for both `localhost` and `127.0.0.1`, then sign in again.
      </Accordion>
    </AccordionGroup>
  </Accordion>
</AccordionGroup>

<Card title="Need help?" href="https://discord.gg/m63hxKsp4p" icon="discord">
  Join our community for Docker deployment support.
</Card>
