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:- Minimal Compose (prebuilt image)
- Build from source
To try the API server without cloning or building, save this single file as Then start it:The
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:${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.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.http://localhost:8000. Interactive docs at http://localhost:8000/docs.
Verify Deployment
After the server starts, check that the API process is reachable:Container Health Status
Thecognee 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 fromtopoteretes/cognee and to inspect its bill of materials:
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:
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:
Additional Information
Docker Compose Services
Docker Compose Services
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.Data Persistence and Host Files
Data Persistence and Host Files
Both images store their data outside the source tree, under The compose file mounts the To ingest files from your host machine, uncomment and update the volume in
/cognee-storage. The Dockerfile and cognee-mcp/Dockerfile bake in these defaults: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.docker-compose.yml.Docker Environment Variables
Docker Environment Variables
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.Database Migrations on Startup
Database Migrations on Startup
Before the API server binds its port, the container entrypoint runs Cognee’s own startup migrations — the same What runs depends on the state of the database it finds:Set
run_migrations() path used by the API server’s lifespan and by cognee-cli. You will see this in the container logs:- Fresh volume / empty database — Cognee creates the missing directories, builds the schema from its models, and stamps
alembic_versionat head instead of replaying the whole revision history. A database is only treated as empty when it has neither auserstable nor analembic_versiontable, 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.
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.Common setups
Common setups
Cognee + PostgreSQL
Cognee + PostgreSQL
PostgreSQL with pgvector is a good production choice for the relational database.Add to your Start both services:
.env:Cognee + PostgreSQL + Neo4j
Cognee + PostgreSQL + Neo4j
For production deployments with a dedicated graph database:Add to your The shipped Start the stack:Neo4j browser is available at
.env:postgres service already mounts the postgres_data volume. Neo4j does not, so add one for graph durability across container recreation:http://localhost:7474.Cognee + ChromaDB
Cognee + ChromaDB
Use ChromaDB as the vector store. The shipped Add to your Start:
docker-compose.yml has no chromadb service, so add one yourself:.env:Cognee + MCP Server
Cognee + MCP Server
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.Cognee + Web UI
Cognee + Web UI
The The backend API and the UI listen on different ports, so they don’t conflict:
ui profile starts the same web interface that cognee.start_ui() launches locally — here it runs as a separate frontend container: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.Managing the Docker Deployment
Managing the Docker Deployment
The If you changed the You only need Stop or remove containers with Docker Compose:
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:docker-compose.yml definition itself (ports, volumes, environment:, profiles), recreate the container instead so the new settings take effect:--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.Optional Extras and Document Loaders
Optional Extras and Document Loaders
The default Docker image includes a fixed set of extras from the repository The build argument is declared in the root 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 Rebuild after updating the
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.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: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):Dockerfile:Bytecode Precompilation
Bytecode Precompilation
The repository then rebuild:
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:Troubleshooting
Troubleshooting
PermissionError with External Databases
PermissionError with External Databases
Even when Cognee is configured to use external databases (Postgres, pgvector, Neo4j, etc.), local writable paths are still required. The usual cause is a host bind mount: named volumes inherit the image’s Fix — mount writable volumes at the image’s storage roots:If you relocate the storage paths with Working Postgres + pgvector + Neo4j compose example — includes healthchecks on both See Storage & Logging for the related env vars, or S3 storage if you want to point these directories at S3 instead of local volumes.
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: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:DATA_ROOT_DIRECTORY and SYSTEM_ROOT_DIRECTORY, mount the volumes at the same paths: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):PostgreSQL Connection Refused
PostgreSQL Connection Refused
When Cognee starts before PostgreSQL finishes initializing, the first API call triggers LLM/embedding connectivity checks (This delays the
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: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.Migration Fails on First Boot
Migration Fails on First Boot
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 adepends_on: condition: service_healthyguard (the shipped compose file already has the healthcheck), as in PostgreSQL Connection Refused.
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.Web UI Login Loops Back to the Login Page
Web UI Login Loops Back to the Login Page
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
frontendservice builds from./cognee-frontendand bind-mountssrc, 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_URLyou 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
localhostand127.0.0.1, then sign in again.
Need help?
Join our community for Docker deployment support.