Run Customermates on your own infrastructure. When to pick self-host, how to install it with Docker Compose in under 15 minutes, and how to operate it day to day.
Self-hosting is two files (docker-compose.yml and .env) plus docker compose up -d. Both files live in the Customermates repo and you fetch them with curl. No git clone, no build step. The published image at ghcr.io/customermates/customermates:latest runs migrations on first boot and ships ready to use.
Both have the same feature set.
| If you… | Pick |
|---|---|
| Want to be using the CRM today | Cloud |
| Have a team of < 20 and no strong data-residency requirement | Cloud |
| Need the CRM on private infra for compliance | Self-host |
| Want to avoid per-seat pricing long-term | Self-host |
| Are an agency managing multiple tenants | Self-host, one instance per tenant |
| Want to contribute to the project | Self-host locally, Cloud for real work |
| Cloud | Self-host | |
|---|---|---|
| Setup time | 2 minutes | ~15 minutes |
| Infra you manage | none | Docker, Postgres, proxy, TLS, backups |
| Updates | automatic | docker compose pull && docker compose up -d |
| EU-hosted | ✓ | wherever you put it |
| Backups | automatic daily | you configure |
| Support SLA | included on paid plans | community |
| Enterprise features (SSO, Audit Log) | paid plan | paid license key |
| Pricing | per seat | free, pay only for Enterprise |
Data custody: Cloud stores data in our EU region, GDPR-compliant, with the usual encryption-at-rest and in-transit. If your compliance posture requires data on your own infra, self-host.
Cost shape: Cloud is predictable per-seat and scales linearly. Self-host is free for the core plus whatever you pay for infra (a small VPS handles hundreds of users). For a 5-person team, cloud is usually cheaper once you factor in the time to run Postgres backups yourself. For a 50-person team, self-host pays off quickly.
You can migrate between them. Export from one, import to the other. The data model is identical. No lock-in either direction.
mkdir customermates && cd customermates
curl -fsSL https://raw.githubusercontent.com/customermates/customermates/main/docker-compose.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/customermates/customermates/main/.env.selfhost.template -o .envThen edit .env with real values:
BETTER_AUTH_SECRET: a long random string (openssl rand -hex 32).CRON_SECRET: another long random string. The webhook-worker sidecar uses this to authenticate against /api/cron/webhook-deliveries. Mismatch means webhooks pile up in pending forever.POSTGRES_PASSWORD: change the default.BASE_URL: your public URL (e.g. https://crm.example.com). Defaults to http://localhost:4000 for local.RESEND_API_KEY and RESEND_FROM_EMAIL: required for signup verification, password reset, and invitation emails.docker compose up -dFirst boot takes a minute while Prisma applies migrations. Watch the logs:
docker compose logs -f appWhen the app is ready, open http://localhost:4000 (or your custom APP_PORT).
Open the URL. Sign up with your email, click the verification link from the inbox, then choose a workspace name. Manage roles for additional users from Company → Users and Company → Roles.
Point your reverse proxy (Caddy, nginx, Traefik) at the app port (4000 by default, or your custom APP_PORT). Caddy example:
crm.example.com {
reverse_proxy localhost:4000
}Customermates sets secure cookies when BASE_URL uses https://. Make sure the proxy forwards X-Forwarded-Proto correctly.
Profile → API Keys → New key. Same flow as cloud. See API keys.
docker compose pull
docker compose up -dPulls the latest app image and restarts the affected services. Migrations run automatically on container boot. Verify with docker compose ps.
docker compose restartRestarts the stack without pulling a new image. Use after .env changes.
docker compose logs -f app
docker compose logs -f postgres
docker compose logs -f webhook-worker
docker compose ps
docker compose exec app shThe webhook-worker container stays quiet on success and prints any non-2xx response ([webhook-worker] HH:MM:SSZ cron returned 401). If you see those lines, the worker's CRON_SECRET doesn't match the app's, or the cron route is otherwise unreachable. If deliveries are stuck pending and the worker logs are empty, the worker is healthy.
docker compose down -v
docker compose up -d-v deletes the Postgres volume. IRREVERSIBLE. Take a backup first if you need the data.
Back up Postgres with pg_dump on a schedule. The app container is stateless.
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" \
| gzip > /var/backups/customermates-$(date +%Y%m%d).sql.gzFor production:
.env and secrets out of source control.Audit Logging, SSO, and White-labeling require an Enterprise license. Everything else is free. Configure the license per the instructions shipped with your license key.