Skip to main content
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 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:
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 image with the default local databases (SQLite, LanceDB, Ladybug), so an LLM API key is the only thing you supply:
Then start it:
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.
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.
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 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.
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:
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:
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 wait on their databases:
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.

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:
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 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 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:
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 addcognifysearch:

Additional Information

Each optional service is gated behind a profile. Use --profile to activate one or more: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.
Both images store their data outside the source tree, under /cognee-storage. The Dockerfile and cognee-mcp/Dockerfile bake in these defaults:
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: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 and PermissionError with External Databases for volume examples.The minimal Compose file 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:
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.
To ingest files from your host machine, uncomment and update the volume in docker-compose.yml.
The cognee container reads configuration from .env at startup. Key variables:ENVIRONMENT is a deprecated alias for ENV, still accepted by the container entrypoints — prefer ENV.See the full list of options in Setup Configuration.
Before the API server binds its port, the container entrypoint runs Cognee’s own startup migrations — the same run_migrations() path used by the API server’s lifespan and by cognee-cli. You will see this in the container logs:
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:
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 if the container exits during this step.
PostgreSQL with pgvector is a good production choice for the relational database.Add to your .env:
Start both services:
For production deployments with a dedicated graph database:Add to your .env:
The shipped postgres service already mounts the postgres_data volume. Neo4j does not, so add one for graph durability across container recreation:
Start the stack:
Neo4j browser is available at http://localhost:7474.
Use ChromaDB as the vector store. The shipped docker-compose.yml has no chromadb service, so add one yourself:
Add to your .env:
Start:
Run the MCP server alongside the API:
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.
The ui profile starts the same web interface that cognee.start_ui() launches locally — here it runs as a separate frontend container:
The backend API and the UI listen on different ports, so they don’t conflict: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.
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() for non-Docker, local Python setups.
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:
If you changed the docker-compose.yml definition itself (ports, volumes, environment:, profiles), recreate the container instead so the new settings take effect:
You only need --build when you change the Dockerfile, its dependencies, or the build arguments passed to it (for example, adding optional extras via COGNEE_EXTRAS) — not for .env edits:
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.
Stop or remove containers with Docker Compose:
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) 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 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:
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.
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.
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:
Then rebuild:
For a table of available extras and common combinations, see Installation. For a table of supported file types and their loaders, see Loaders.For example, the docs extra adds UnstructuredLoader, 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):
Rebuild after updating the Dockerfile:
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:
then rebuild:
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:
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:
Fix — mount writable volumes at the image’s storage roots:
If you relocate the storage paths with DATA_ROOT_DIRECTORY and SYSTEM_ROOT_DIRECTORY, mount the volumes at the same paths:
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):
See Storage & Logging for the related env vars, or S3 storage if you want to point these directories at S3 instead of local volumes.
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:
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.
The entrypoint runs startup migrations 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.
  • 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.
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:
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.
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.

Need help?

Join our community for Docker deployment support.