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:

Troubleshooting

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.... Three 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.
  • The Postgres connection settings are incomplete. With DB_PROVIDER=postgres, the port is cast with int() while the engine is built, and DB_PORT has no application-level default — the shipped compose file supplies 5432, so this typically hits direct docker run or platform deployments. Leaving it unset aborts the boot with TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType', most often because the connection was supplied as a single platform-injected DATABASE_URL, which Cognee does not read. See DB_PORT unset for the split DB_* variables to set instead.
The first two are safe to retry: restart the container once the volume or database is ready and the migration runs again from where it left off. The third needs the environment fixed first.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 the image. The frontend service runs the published cognee/cognee-ui image, so an old local copy of a moving tag keeps serving old code: docker compose --profile ui pull frontend && docker compose --profile ui up -d frontend. On the ui-dev profile, which builds from ./cognee-frontend and bind-mounts src, pull your checkout and rebuild instead: docker compose --profile ui-dev up -d --build frontend-dev.
  • Check any COGNEE_BACKEND_URL or NEXT_PUBLIC_LOCAL_API_URL you set. Either one wins over the browser-derived host — COGNEE_BACKEND_URL first — so it reintroduces the mismatch if its hostname differs from the one in your address bar. curl -s http://localhost:3000/api/runtime-config shows what the container resolved.
  • 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.
A common solo-operator setup: one self-hosted Cognee, an agent connected over MCP writing to its own dataset, and you signing into the web UI as the admin superuser — where the datasets page is empty, even though the agent’s remember calls succeed and its data is intact.This is access control working as designed, not a failed ingestion. With ENABLE_BACKEND_ACCESS_CONTROL=True (the Docker default — see the Docker Environment Variables accordion under Additional Information), every dataset listing — including the UI’s datasets page, backed by GET /api/v1/datasets — returns only datasets the authenticated user holds explicit read permission on. Superuser status does not bypass dataset permissions: it covers user, tenant, and role management and system settings, not dataset access. A dataset created by an agent user grants read/write/delete/share to that agent alone, so the admin account has no path to it.Two fixes — one for datasets that already exist, one for every dataset created after it:
  1. Grant yourself access to the agent’s existing dataset. Only a principal holding share on a dataset can grant permissions on it, and that is the agent, not you. So make the request authenticated as the agent (its API key or credentials), granting your admin user id read — plus write and delete if you want to manage the dataset from the UI:
    The agent’s GET /api/v1/datasets gives the dataset id; your admin user id comes from GET /api/v1/users/me.
  2. Create agent identities as children of your admin user. cognee.agents.create() — or create_user(..., parent_user_id=<your-user-id>) — mints the agent with parent_user_id pointing at you, and every dataset the agent creates from then on automatically grants the parent full permissions. parent_user_id can only be set when the user is created — PATCH /api/v1/users/{id} does not accept it — so an existing standalone agent user cannot be re-parented: grant per-dataset access as above, or recreate the agent as a child (which rotates its API key).
The same grants are how two agents share one brain — the dataset’s owner grants the second agent read and write on it. See Permission Snippets for the full patterns.

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).ui and ui-dev both publish host port 3000, so they are alternatives rather than additions — activating both fails on the port collision.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 and builds the schema by running the entire Alembic migration chain; nothing is stamped, so alembic_version only records revisions that actually executed. A database is only treated as empty when Cognee inspected it and found neither a users table nor an alembic_version table, or when it is a local SQLite database whose file does not exist yet — so a pre-Alembic legacy database is migrated rather than mistaken for an empty one.
  • Existing database — Alembic applies the pending relational revisions, then the graph/vector data migration chain runs.
  • Database Cognee cannot inspect — a database that is unreachable, still starting up, or rejecting the connection is not counted as empty. The inspection error aborts the boot instead of trying to build the schema from scratch against a database that may simply not be ready yet. Restart the container once the database is ready — see Troubleshooting → Migration Fails on First Boot.
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, pulled from the published cognee/cognee-ui image rather than built from your checkout:
The container waits for the cognee service’s healthcheck to pass before it starts (depends_on: condition: service_healthy), so the UI does not come up against a backend that is still booting.The backend API and the UI listen on different ports, so they don’t conflict:The compose file defaults to cognee/cognee-ui:latest. Override it with COGNEE_UI_TAG to pin a different tag:
Every push to main publishes latest, a main branch tag, and a main-<short-sha> tag; releases add the release version. The image is built for linux/amd64 and linux/arm64 and runs as a non-root user.Pointing the UI at a backend. The frontend service passes COGNEE_BACKEND_URL through from your environment or .env, and the image reads it at run time on every request — which is what lets one prebuilt image serve any backend without a rebuild. Leave it unset for the default Compose setup: the browser then derives the backend host from the address you loaded the UI from, on port 8000, so localhost and 127.0.0.1 both work. Set it when the API is reachable somewhere else:
The browser calls the backend directly, so this must be the address as seen from the browser — never the cognee service name, which only resolves inside cognee-network.Remote backend and CORS. When the UI and the API run on different hosts, pointing the UI at the backend is only half the wiring — the backend must also allow the UI’s origin through its CORS policy, or the browser refuses every call with the UI’s generic “Cannot connect” error:
CORS_ALLOWED_ORIGINS is a comma-separated list and must name the exact origin in your address bar, scheme and port included. The shipped compose file defaults it to *, so this only needs attention when you narrow it or start the backend some other way — a backend launched without it allows only UI_APP_URL, which defaults to http://localhost:3000, so a remote UI is refused until you set one of the two.Behind a reverse proxy that terminates TLS, use the proxy’s public URLs instead (COGNEE_BACKEND_URL: https://cognee-api.example.com, CORS_ALLOWED_ORIGINS: https://cognee-ui.example.com) and keep port 8000 unpublished on the backend host.
The frontend container exits immediately. The image validates COGNEE_BACKEND_URL in its entrypoint and refuses to start on a value it cannot use, rather than booting and answering every request with a 500. docker compose logs frontend shows the reason:
The value must include an http:// or https:// scheme. An unset or empty value is fine — that is the default — so this only ever fires on a value you set. Because the service runs with restart: always, a bad value shows up as a crash loop.
The container’s healthcheck probes GET /api/runtime-config, which also serves as a manual check that the backend URL resolved the way you expect:
Working on the frontend. Use ui-dev instead, which builds the hot-reloading dev stage of cognee-frontend/Dockerfile from your checkout and bind-mounts src and public, so edits show up without a rebuild:
It serves on the same host port 3000 and takes the same COGNEE_BACKEND_URL. Unlike frontend, it has no depends_on guard, so start the backend yourself if you need it up first.
Don’t also call cognee.start_ui() while the ui (or ui-dev) 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, dlt, 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:
Because dlt is among the defaults, the image ingests .csv files through the structured dlt route: the loader engine registers dlt_csv_loader above the plain-text csv_loader whenever dlt is importable, so a CSV added through the container is staged through dlt and its rows skip chunking and LLM entity extraction, as described under CSV Files, with no custom build. Flattening a CSV into plain text instead is possible only from the Python SDK, by requesting csv_loader explicitly for that call with preferred_loaders=[{"csv_loader": {}}]; POST /api/v1/add accepts no loader override, so every CSV uploaded to the image over HTTP takes the dlt route. The trade-off is footprint: the extra pulls dlt[sqlalchemy] and pandas into the image.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:

Need help?

Join our community for Docker deployment support.