# Self-hosting

> Running Comity on your own single host with Docker Compose, for Enterprise pilots.

> [!IMPORTANT]
> Self-hosting is available for **Enterprise pilots**, set up together with us. There is no self-serve installer, license or high-availability setup. Contact [hello@comity.so](mailto:hello@comity.so) to discuss a self-hosted deployment.

This page describes what the deployment consists of, so you can judge whether it fits your infrastructure.

## What runs

Everything runs from one Docker Compose file on one Linux host (sized for 2 GB RAM plus 2 GB swap):

| Service | Role |
|---|---|
| `postgres` | PostgreSQL 18. Not published outside the Compose network. |
| `migrate` | Applies database migrations on every start; `api` and `worker` start only after it succeeds. |
| `api` | The REST API, MCP endpoint and webhooks receiver (port 8080, internal). |
| `worker` | Background jobs: expiry sweeps, event partitions, notification delivery, GitHub sync, Stripe usage reports. |
| `caddy` | TLS termination on ports 80/443 with automatic Let's Encrypt certificates; serves the CLI tarball under `/cli/`. |

The web dashboard (sign-in, browser login approval for `comity login`, Messages, Settings) is not part of this Compose stack; its address is configured with `WEB_URL`. Plan where it runs with us. Without it, developers sign in with tokens created by the admin command (`comity login --token cmt_…`).

The domain's DNS must point straight at the host (no proxying CDN in front): Caddy obtains its own certificate, and long-lived MCP and SSE streams must pass through unbuffered.

## Configuration

A provisioning script writes `/opt/comity/.env` once with random database passwords and a master key. The server reads:

| Variable | Required | Default | Purpose |
|---|---|---|---|
| `DATABASE_URL` | yes | | Owner connection (migrations, sign-in, background sweeps) |
| `APP_DATABASE_URL` | yes | | Application role `comity_app`, subject to row-level security |
| `COMITY_MASTER_KEY` | yes | | 32 bytes, base64. Wraps every org's data key. |
| `PUBLIC_URL` | for GitHub App | | The API's public URL |
| `WEB_URL` | | `https://comity.so` | Dashboard origin (CORS, invite and device-login links) |
| `PORT` / `HOST` | | `8080` / `0.0.0.0` | |
| `LOG_LEVEL` | | `info` | |
| `RESEND_API_KEY`, `EMAIL_FROM` | for email | | Email sign-up, password reset and invite emails |
| `GITHUB_OAUTH_CLIENT_ID`, `GITHUB_OAUTH_CLIENT_SECRET` | for GitHub login | | |
| `STRIPE_*` | no | | Billing; off unless `STRIPE_SECRET_KEY` is set |
| `EGRESS_ALLOW_PRIVATE` | | `false` | Allow webhook and Slack URLs on private networks |
| `COMITY_DOMAIN` | | | Caddy's site address |

> [!CAUTION]
> Back up `COMITY_MASTER_KEY` off the host and never change it in place. Losing it, or replacing it without re-wrapping the org keys, makes all encrypted content unreadable, including in backups.

## First org

The server has an admin command instead of an admin UI. Bootstrapping prints the owner's token once:

```sh
docker compose run --rm migrate node dist/server/admin.js bootstrap --org acme --name "Acme Inc" --email owner@acme.example
```

Other admin commands: `create-member`, `create-token`, and `set-plan --org <slug> --plan <free|team|business|enterprise> [--developer-limit n|unlimited] [--repo-limit n|unlimited]` (custom limits apply to Enterprise only).

Developers then point the CLI at your server:

```sh
npm i -g https://comity.example.com/cli/comity-cli.tgz
comity login --server https://comity.example.com
```

`comity update` reads the release manifest from your server (`/cli/latest.json`), so self-hosted machines update from your host.

## Backups

A systemd timer runs a `pg_dump` daily at 03:17 UTC to `/opt/comity/backups/` and keeps 14 days. Copy dumps off the host. A backup is only restorable together with the master key that was active when it was taken.
