Sign inSign up

bearlypro/khirby

By bearlypro

•Updated 4 days ago

Self-hosted single-tenant CRM (Vue + NestJS).

Buildkit cache
Image
0

3.3K

bearlypro/khirby repository overview

⁠Khirby

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.

docker pull bearlypro/khirby:latest

Tags: latest · git-<sha> · vX.Y.Z (CI / release tags)

⁠Docs
PageUse
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

⁠Requirements

DependencyRole
Docker Engine 24+ (Compose v2)Run the stack
PostgreSQL 16+Primary database
Redis 7+Session store
HTTPS reverse proxyProduction cookies use secure: true — terminate TLS in front (Caddy / nginx / Traefik)
~2 GB RAMComfortable for app + DB + Redis

Run one app replica only. Migrations run on container start; multiple replicas can race.

Health check: GET /api/health


⁠Environment variables

Full list and notes: Self-hosted installation⁠. Template: docker/crm.env.example⁠.

⁠Required (app container)
VariableDescription
DATABASE_URLPostgres URL, e.g. postgres://crm:SECRET@db:5432/crm
REDIS_URLRedis URL, e.g. redis://redis:6379
SESSION_SECRETCookie signing secret — ≥ 32 characters (fail-fast in production if missing/short)
CORS_ORIGINComma-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_EMAILBootstrap admin (created on first start if no users exist)
ADMIN_PASSWORDBootstrap 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.
VariableRequired?Description
NODE_ENVRecommendedSet to production
PORTOptionalNest listen port inside the image (default 3000; nginx proxies to it)
APP_URLIf using Google mailbox / Swarm TraefikPublic 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_KEYIf using firm mailbox64-char hex — encrypts IMAP/SMTP passwords and Google refresh tokens (AES-256-GCM)
GOOGLE_MAIL_CLIENT_IDOptionalGmail / Workspace OAuth (XOAUTH2)
GOOGLE_MAIL_CLIENT_SECRETOptionalPair with GOOGLE_MAIL_CLIENT_ID
AI_COMPOSE_SECRETS_KEYIf using AI Compose plugin64-char hex — encrypts LLM API keys at rest
POKELO_SECRETS_KEYIf using Pokelo plugin64-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⁠.

⁠Swarm / Komodo stack helpers

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:

VariableDescription
DATA_PATHBind-mount root for Postgres/Redis data, e.g. /srv/container-data/khirby
POSTGRES_USERPostgres user (default crm)
POSTGRES_PASSWORDPostgres password
POSTGRES_DBDatabase name (default crm)

⁠Quick start (Compose)

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

⁠Image notes

  • Bundle: nginx (SPA) + Nest API; exposed port 80.
  • Default first-party plugins are installed from npm at build time (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⁠.
  • Prefer pinning a release tag (vX.Y.Z) in production; use latest only if you accept rolling updates.
  • Multi-arch (linux/amd64, linux/arm64) is published from CI on version tags; some Hub tags may be amd64-only.

⁠Support & contributing

Tag summary

Content type

Image

Digest

sha256:b3403bcf6…

Size

113.9 MB

Last updated

4 days ago

docker pull bearlypro/khirby