Deployment

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 .env

Set 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 start

pnpm 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

ServicePurpose
migrateRuns the database migrations as the Postgres owner, then gives access to ctxpipe_app
backendThe public API, auth, MCP, callbacks, and the UI proxy. Published on host port 3000.
uiThe web app, served through the backend
workerThe background jobs for ingestion and connectors
codesearchThe repository clone cache and the Zoekt indexes. Keep one replica.
postgresThe application database
falkordbThe default graph database
dindThe Docker daemon for chat and ingestion sandboxes
chat-sandbox-imageOne-shot: builds the sandbox image inside dind
agent-vaultAdds credentials to sandbox requests. It is the only address that sandboxes can reach.
agent-vault-secretsOne-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_URL and EMAIL_FROM_ADDRESS. Without them, ctxpipe sends no verification, password reset, or invitation email;
  • social sign-in: GITHUB_CLIENT_ID, GOOGLE_*, and MICROSOFT_*;
  • Atlassian OAuth: ATLASSIAN_CLIENT_ID and ATLASSIAN_CLIENT_SECRET;
  • an external graph database: GRAPH_DB_URI, GRAPH_DB_PROVIDER, GRAPH_DB_USER, and GRAPH_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 -v deletes it. After docker 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.

StateWhere it isBack it up
Postgres dataAn anonymous volume of the postgres container, or your named volumeYes
Graph dataThe falkordb container, or your named volumeYes, or rebuild it from the Workspace repositories
Repository clone cacherepo_cacheNo, codesearch clones again
Zoekt indexeszoekt_index_deployNo, codesearch indexes again
Agent Vault passwords and dataagent_vault_master, agent_vault_owner, agent_vault_dataKeep them. New passwords do not match an existing Agent Vault.
Sandboxes and TLS certificatessandbox_docker, sandbox_ca, sandbox_client_certsNo
Secrets.env, or your secret storeYes

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:

  1. Change the owner password in Postgres: docker compose exec postgres psql -U ctxpipe -c "ALTER ROLE ctxpipe PASSWORD '<new>'". Postgres reads POSTGRES_PASSWORD only when it creates the volume.
  2. Set the same value as POSTGRES_PASSWORD in .env.
  3. Run docker compose --profile deploy up -d. migrate sets the new ctxpipe_app password, 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.

  • dind is 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 2376 inside 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, and worker share the sandbox network, 172.30.99.0/24. If it overlaps a network of your host, change each 172.30.99.x address in docker-compose.yml.
  • chat-sandbox-image builds the sandbox image on each pnpm start. Until the image exists, chat answers 503, and the rest of the app works.
  • On a cgroup v2 host, all sandboxes share one limit of 8192 processes. Without cgroup v2, dind logs a warning and applies no limit.
  • Container logs rotate at 3 files of 10 MB.
  • If you remove sandbox_ca, dind makes new certificates. Then restart backend and worker.

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: 8g

The 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 ps shows healthy services and a healthy dind.
  • curl https://<public-origin>/.status returns ok.
  • You can sign in through the public origin.
  • A linked repository reaches Indexed.
  • Workspace chat answers, and docker compose exec dind docker ps shows its sandbox.
  • An MCP client reaches {AUTH_BASE_URL}/mcp.

See Operations for the sandbox disk and health checks.