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:
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.... 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 adepends_on: condition: service_healthyguard (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 withint()while the engine is built, andDB_PORThas no application-level default — the shipped compose file supplies5432, so this typically hits directdocker runor platform deployments. Leaving it unset aborts the boot withTypeError: 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-injectedDATABASE_URL, which Cognee does not read. SeeDB_PORTunset for the splitDB_*variables to set instead.
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 the image. The
frontendservice runs the publishedcognee/cognee-uiimage, 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 theui-devprofile, which builds from./cognee-frontendand bind-mountssrc, pull your checkout and rebuild instead:docker compose --profile ui-dev up -d --build frontend-dev. - Check any
COGNEE_BACKEND_URLorNEXT_PUBLIC_LOCAL_API_URLyou set. Either one wins over the browser-derived host —COGNEE_BACKEND_URLfirst — so it reintroduces the mismatch if its hostname differs from the one in your address bar.curl -s http://localhost:3000/api/runtime-configshows 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
localhostand127.0.0.1, then sign in again.
Admin Can't See Agent Datasets in the Web UI
Admin Can't See Agent Datasets in the Web UI
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:-
Grant yourself access to the agent’s existing dataset. Only a principal holding
shareon 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 idread— pluswriteanddeleteif you want to manage the dataset from the UI:The agent’sGET /api/v1/datasetsgives the dataset id; your admin user id comes fromGET /api/v1/users/me. -
Create agent identities as children of your admin user.
cognee.agents.create()— orcreate_user(..., parent_user_id=<your-user-id>)— mints the agent withparent_user_idpointing at you, and every dataset the agent creates from then on automatically grants the parent full permissions.parent_user_idcan 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).
read and write on it. See Permission Snippets for the full patterns.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).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.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 and builds the schema by running the entire Alembic migration chain; nothing is stamped, so
alembic_versiononly records revisions that actually executed. A database is only treated as empty when Cognee inspected it and found neither auserstable nor analembic_versiontable, 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.
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 container waits for the Every push to The browser calls the backend directly, so this must be the address as seen from the browser — never the Working on the frontend. Use It serves on the same host port
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: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: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: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:- On the UI host
- On the backend host
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 container’s healthcheck probes GET /api/runtime-config, which also serves as a manual check that the backend URL resolved the way you expect: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: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.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 Because 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, 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: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.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:Need help?
Join our community for Docker deployment support.