Docker
Run self-hosted ctxpipe with the Docker Compose deploy profile.
Docker Compose is the reference self-hosted path. Use it for an evaluation, for production-style tests, and for a small internal deployment on one host.
Start the stack
You need Docker with Compose, Node.js, and pnpm. From a clone of the repository:
git clone https://github.com/ctxpipe-ai/ctxpipe.git
cd ctxpipe
cp docker-compose.env.example .envSet at least these values in .env:
AUTH_SECRET=<32-plus-character-secret>
AUTH_BASE_URL=https://ctx.example.com
CTXPIPE_PUBLIC_APP_URL=https://ctx.example.com
MODEL_PROVIDER=openrouter
MODEL_PROVIDER_API_KEY=<model-provider-key>Then start the deploy profile:
pnpm startpnpm start runs docker compose --profile deploy up -d. It builds the
images only when they do not exist.
For a local evaluation, AUTH_BASE_URL and CTXPIPE_PUBLIC_APP_URL can be
http://localhost:3000. For team use, put a TLS proxy in front of the backend
and use the public HTTPS origin. The UI image contains CTXPIPE_PUBLIC_APP_URL
at build time. After you change it, build the images again with
docker compose --profile deploy up -d --build.
What the deploy profile starts
| Service | Purpose |
|---|---|
migrate | Runs the database migrations as the Postgres owner, then gives access to ctxpipe_app |
backend | The public API, auth, MCP, callbacks, and the UI proxy. Published on host port 3000. |
ui | The web app, served through the backend |
worker | The background jobs for ingestion and connectors |
codesearch | The repository clone cache and the Zoekt indexes. Keep one replica. |
postgres | The application database |
falkordb | The default graph database |
dind | The Docker daemon for chat and ingestion sandboxes |
chat-sandbox-image | One-shot: builds the sandbox image inside dind |
agent-vault | Adds credentials to sandbox requests. It is the only address that sandboxes can reach. |
agent-vault-secrets | One-shot: generates the Agent Vault passwords |
The backend is the only service on a public host port (3000). Compose binds
codesearch (3001) and ui (3002) to 127.0.0.1 only. Postgres and
FalkorDB have no host port.
Settings that need an override file
The deploy profile passes only a fixed list of variables from .env to each
service: the auth, model, OpenTelemetry, and sandbox settings, and the Slack,
Linear, Notion, and PagerDuty connector secrets. Other values in .env do not
reach the services. Add them to the environment of backend and worker in
a docker-compose.override.yml. This applies to:
- email:
SMTP_CONNECTION_URLandEMAIL_FROM_ADDRESS. Without them, ctxpipe sends no verification, password reset, or invitation email; - social sign-in:
GITHUB_CLIENT_ID,GOOGLE_*, andMICROSOFT_*; - Atlassian OAuth:
ATLASSIAN_CLIENT_IDandATLASSIAN_CLIENT_SECRET; - an external graph database:
GRAPH_DB_URI,GRAPH_DB_PROVIDER,GRAPH_DB_USER, andGRAPH_DB_PASSWORD.
services:
backend:
environment:
SMTP_CONNECTION_URL: ${SMTP_CONNECTION_URL}
EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS}
worker:
environment:
SMTP_CONNECTION_URL: ${SMTP_CONNECTION_URL}
EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS}The in-product GitHub wizard stores the GitHub App credentials in the database, so the GitHub App needs no variable.
Persistent state
Keep the database data on named volumes
The deploy profile mounts no named volume for postgres or falkordb:
- Postgres keeps its data in an anonymous Docker volume.
docker compose down -vdeletes it. Afterdocker compose down, the new container does not use the old volume. - The FalkorDB image declares no volume, so FalkorDB keeps its data in the
writable layer of the container. Each time Compose creates the container
again, for example after
down, an image update, or a configuration change, the graph data is lost.
Before people depend on the deployment, mount named volumes in
docker-compose.override.yml:
services:
postgres:
volumes:
- postgres_data:/var/lib/postgresql/data
falkordb:
volumes:
- falkordb_data:/var/lib/falkordb/data
volumes:
postgres_data:
falkordb_data:Do this before the first start. On a running deployment, first back up the data, then restore it into the new volumes.
| State | Where it is | Back it up |
|---|---|---|
| Postgres data | An anonymous volume of the postgres container, or your named volume | Yes |
| Graph data | The falkordb container, or your named volume | Yes, or rebuild it from the Workspace repositories |
| Repository clone cache | repo_cache | No, codesearch clones again |
| Zoekt indexes | zoekt_index_deploy | No, codesearch indexes again |
| Agent Vault passwords and data | agent_vault_master, agent_vault_owner, agent_vault_data | Keep them. New passwords do not match an existing Agent Vault. |
| Sandboxes and TLS certificates | sandbox_docker, sandbox_ca, sandbox_client_certs | No |
| Secrets | .env, or your secret store | Yes |
To back up Postgres, run docker compose exec postgres pg_dump -U ctxpipe ctxpipe > backup.sql.
Database passwords
Compose sets two Postgres URLs for you. migrate connects as POSTGRES_USER
(default ctxpipe). The other services connect as ctxpipe_app. Both use
POSTGRES_PASSWORD. To change the password:
- Change the owner password in Postgres:
docker compose exec postgres psql -U ctxpipe -c "ALTER ROLE ctxpipe PASSWORD '<new>'". Postgres readsPOSTGRES_PASSWORDonly when it creates the volume. - Set the same value as
POSTGRES_PASSWORDin.env. - Run
docker compose --profile deploy up -d.migratesets the newctxpipe_apppassword, and the services restart with it.
Chat sandboxes
Each conversation and each ingestion runs in its own container in dind, a
Docker-in-Docker daemon. It needs no settings.
dindis the only privileged container. The sandboxes are not privileged, and the backend never mounts the Docker socket of the host.- The Docker API listens on port
2376inside Compose, with mutual TLS. It is not published on the host. - Sandboxes hold no credential, and can reach only Agent Vault. See the sandbox network policy.
dind,agent-vault,backend, andworkershare thesandboxnetwork,172.30.99.0/24. If it overlaps a network of your host, change each172.30.99.xaddress indocker-compose.yml.chat-sandbox-imagebuilds the sandbox image on eachpnpm start. Until the image exists, chat answers503, and the rest of the app works.- On a cgroup v2 host, all sandboxes share one limit of 8192 processes. Without
cgroup v2,
dindlogs a warning and applies no limit. - Container logs rotate at 3 files of 10 MB.
- If you remove
sandbox_ca,dindmakes new certificates. Then restartbackendandworker.
To limit the memory of all sandboxes, limit dind in
docker-compose.override.yml. The sandboxes together get at most 85% of it:
services:
dind:
mem_limit: 8gThe Docker client certificate gives root on dind
Each process that has the files in sandbox_client_certs can start
privileged containers on dind, which is root on the Docker host. Do not
mount that volume in another service, and do not copy it off the host.
If dind stops, chat answers 503 until it is back. It never runs an agent
without a sandbox. The backend and worker images refuse
SANDBOX_PROVIDER=unsandboxed, because an agent without a sandbox has the
credentials of the backend. If your host cannot run privileged containers, chat
is not available.
To read a plain git repository on a private git server, see Plain git repositories.
Check the deployment
docker compose --profile deploy psshows healthy services and a healthydind.curl https://<public-origin>/.statusreturnsok.- You can sign in through the public origin.
- A linked repository reaches Indexed.
- Workspace chat answers, and
docker compose exec dind docker psshows its sandbox. - An MCP client reaches
{AUTH_BASE_URL}/mcp.
See Operations for the sandbox disk and health checks.