# Docker (https://docs.ctxpipe.ai/docs/self-hosting/deployment/docker)

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:

```bash
git clone https://github.com/ctxpipe-ai/ctxpipe.git
cd ctxpipe
cp docker-compose.env.example .env
```

Set at least these values in `.env`:

```bash
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:

```bash
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

| 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_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`.

```yaml
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

<Callout type="warn" title="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`:

  ```yaml
  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.
</Callout>

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

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](/docs/self-hosting/architecture#chat-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:

```yaml
services:
  dind:
    mem_limit: 8g
```

<Callout type="warn" title="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.
</Callout>

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](/docs/self-hosting/configuration#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](/docs/self-hosting/operations#chat-sandboxes-docker-compose)
for the sandbox disk and health checks.

<Cards>
  <Card title="Configuration" href="/docs/self-hosting/configuration" description="Variables and connector setup." />

  <Card title="Production readiness" href="/docs/self-hosting/production-readiness" description="TLS, callbacks, persistence, and secrets." />

  <Card title="Operations" href="/docs/self-hosting/operations" description="Health checks, logs, backups, and recovery." />
</Cards>
