Self-hosted single-tenant CRM (Vue + NestJS).
3.3K
Self-hosted, single-tenant CRM: Vue 3 SPA + NestJS (Fastify) API in one container.
nginx serves the UI and reverse-proxies /api to the Nest process. The container listens on port 80.
| Image | bearlypro/khirby |
| License | MIT |
| Source | github.com/Khirby-labs/khirby |
| Plugins | github.com/Khirby-labs/plugins |
| Docs | khirby.com/docs |
| Deploy guide | Self-hosted installation |
| Env template | docker/crm.env.example |
docker pull bearlypro/khirby:latest
Tags: latest · git-<sha> · vX.Y.Z (CI / release tags)
| Page | Use |
|---|---|
| Getting started | Local pnpm stack vs production image |
| Self-hosted installation | Compose, env, TLS, backup, troubleshooting |
| Architecture | Request flow, auth, container layout |
| Connect a Google mailbox | Gmail / Workspace OAuth (XOAUTH2) |
| Connect Listmonk | Newsletters (plugin) |
| Install a plugin | Operators — plugins.manifest.json |
| Create a plugin | Authors — SDK / host API |
| Landing forms | @khirby/forms-* on public npm |
| Dependency | Role |
|---|---|
| Docker Engine 24+ (Compose v2) | Run the stack |
| PostgreSQL 16+ | Primary database |
| Redis 7+ | Session store |
| HTTPS reverse proxy | Production cookies use secure: true — terminate TLS in front (Caddy / nginx / Traefik) |
| ~2 GB RAM | Comfortable for app + DB + Redis |
Run one app replica only. Migrations run on container start; multiple replicas can race.
Health check: GET /api/health
Full list and notes: Self-hosted installation. Template: docker/crm.env.example.
| Variable | Description |
|---|---|
DATABASE_URL | Postgres URL, e.g. postgres://crm:SECRET@db:5432/crm |
REDIS_URL | Redis URL, e.g. redis://redis:6379 |
SESSION_SECRET | Cookie signing secret — ≥ 32 characters (fail-fast in production if missing/short) |
CORS_ORIGIN | Comma-separated allowlist for credentialed API, e.g. https://crm.example.com. Required in production; * / empty reject startup. Public /api/public/* stays open. Include landing origins if you embed forms. |
ADMIN_EMAIL | Bootstrap admin (created on first start if no users exist) |
ADMIN_PASSWORD | Bootstrap admin password |
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" # SESSION_SECRET
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # MAIL_SECRETS_KEY etc.
| Variable | Required? | Description |
|---|---|---|
NODE_ENV | Recommended | Set to production |
PORT | Optional | Nest listen port inside the image (default 3000; nginx proxies to it) |
APP_URL | If using Google mailbox / Swarm Traefik | Public hostname, e.g. crm.example.com (no https://). App uses it for the OAuth callback; the Swarm stack also uses it for Traefik. |
MAIL_SECRETS_KEY | If using firm mailbox | 64-char hex — encrypts IMAP/SMTP passwords and Google refresh tokens (AES-256-GCM) |
GOOGLE_MAIL_CLIENT_ID | Optional | Gmail / Workspace OAuth (XOAUTH2) |
GOOGLE_MAIL_CLIENT_SECRET | Optional | Pair with GOOGLE_MAIL_CLIENT_ID |
AI_COMPOSE_SECRETS_KEY | If using AI Compose plugin | 64-char hex — encrypts LLM API keys at rest |
POKELO_SECRETS_KEY | If using Pokelo plugin | 64-char hex — encrypts Pokelo MCP token at rest |
Google OAuth redirect URI (when enabled):
https://<your-host>/api/mail/mailbox/oauth/google/callback
Setup: Connect a Google mailbox.
Used by docker/docker-stack.yml to build DATABASE_URL / REDIS_URL (and Traefik host). The app image itself reads DATABASE_URL, REDIS_URL, and APP_URL — not these helpers:
| Variable | Description |
|---|---|
DATA_PATH | Bind-mount root for Postgres/Redis data, e.g. /srv/container-data/khirby |
POSTGRES_USER | Postgres user (default crm) |
POSTGRES_PASSWORD | Postgres password |
POSTGRES_DB | Database name (default crm) |
Minimal production-shaped stack (TLS in front; bind app to localhost). Step-by-step: Self-hosted installation.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-crm}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-crm}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-crm} -d ${POSTGRES_DB:-crm}']
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: ['redis-server', '--appendonly', 'yes']
volumes:
- redis_data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
app:
image: bearlypro/khirby:latest
environment:
NODE_ENV: production
PORT: '3000'
DATABASE_URL: postgres://${POSTGRES_USER:-crm}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-crm}
REDIS_URL: redis://redis:6379
SESSION_SECRET: ${SESSION_SECRET}
CORS_ORIGIN: ${CORS_ORIGIN}
ADMIN_EMAIL: ${ADMIN_EMAIL}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
APP_URL: ${APP_URL:-}
MAIL_SECRETS_KEY: ${MAIL_SECRETS_KEY:-}
GOOGLE_MAIL_CLIENT_ID: ${GOOGLE_MAIL_CLIENT_ID:-}
GOOGLE_MAIL_CLIENT_SECRET: ${GOOGLE_MAIL_CLIENT_SECRET:-}
AI_COMPOSE_SECRETS_KEY: ${AI_COMPOSE_SECRETS_KEY:-}
POKELO_SECRETS_KEY: ${POKELO_SECRETS_KEY:-}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- '127.0.0.1:8080:80'
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1/api/health || exit 1']
interval: 10s
timeout: 5s
retries: 5
start_period: 120s
volumes:
postgres_data:
redis_data:
Example .env:
CORS_ORIGIN=https://crm.example.com
APP_URL=crm.example.com
POSTGRES_USER=crm
POSTGRES_PASSWORD=change-me-strong-db-password
POSTGRES_DB=crm
SESSION_SECRET=change-me-to-a-random-string-at-least-32-chars
[email protected]
ADMIN_PASSWORD=change-me-strong-admin-password
docker compose pull
docker compose up -d
curl -sS http://127.0.0.1:8080/api/health
Point TLS at 127.0.0.1:8080. SPA and API must share the same hostname. Caddy / nginx examples: Self-hosted — HTTPS.
For Swarm / Traefik / Komodo, use the stack file in the repo:
cp docker/crm.env.example docker/crm.env # fill secrets
docker stack deploy --with-registry-auth --env-file docker/crm.env \
-c docker/docker-stack.yml khirby
plugins.manifest.json: webhook, discord, listmonk, mcp, ai-compose, pokelo). Extra plugins need a custom image or a source deploy until the marketplace ships — Install a plugin.vX.Y.Z) in production; use latest only if you accept rolling updates.linux/amd64, linux/arm64) is published from CI on version tags; some Hub tags may be amd64-only.@khirby/plugin-sdk / @khirby/plugin-host on npmContent type
Image
Digest
sha256:b3403bcf6…
Size
113.9 MB
Last updated
4 days ago
docker pull bearlypro/khirby