Skip to main content

How to Self-Host Twenty CRM — Open Source HubSpot Alternative 2026

·OSSAlt Team
twenty-crmhubspot-alternativeself-hostingcrmdocker2026

What Is Twenty CRM?

Twenty is a modern open source CRM built with a product philosophy closer to Notion or Linear than to Salesforce. It's extensible via a custom objects system, has a clean kanban + table view interface, and is designed to be a platform you can build on top of — not just use.

HubSpot's CRM Suite costs $90/month for 2 users (Starter) and scales steeply. Salesforce starts at $25/user/month but quickly reaches hundreds. Twenty is free when self-hosted — just server costs (~$10/month).

Key features:

  • Contacts, Companies, Deals pipeline
  • Custom objects (extend the data model without code)
  • Kanban, table, and timeline views
  • Activity timeline (emails, calls, meetings)
  • Workflow automation (beta)
  • Developer API (GraphQL)
  • Zapier-like automation via integrations
  • GitHub Stars: 25k+ (one of the fastest-growing OSS CRMs)

Prerequisites

  • VPS with 2 vCPU, 2GB RAM (Hetzner CX32 ~€5.49/month)
  • Docker + Docker Compose v2
  • Domain name pointing to your server
  • SMTP for email (optional but recommended)

Docker Compose Deployment

1. Create Project Directory

mkdir twenty && cd twenty

2. Create docker-compose.yaml

version: "3.8"

networks:
  twenty:

volumes:
  twenty-db-data:
  twenty-server-local-data:

services:
  twenty-change-detect:
    image: twentycrm/twenty:latest
    restart: unless-stopped
    networks:
      - twenty
    depends_on:
      twenty-db:
        condition: service_healthy
    env_file: .env
    command: ["yarn", "command:prod", "upgrade"]

  twenty-server:
    image: twentycrm/twenty:latest
    restart: unless-stopped
    networks:
      - twenty
    ports:
      - "3000:3000"
    depends_on:
      twenty-change-detect:
        condition: service_completed_successfully
    env_file: .env
    volumes:
      - twenty-server-local-data:/app/packages/twenty-server/.local-storage

  twenty-worker:
    image: twentycrm/twenty:latest
    restart: unless-stopped
    networks:
      - twenty
    depends_on:
      twenty-server:
        condition: service_started
    env_file: .env
    command: ["yarn", "command:prod", "worker"]

  twenty-db:
    image: twentycrm/twenty-postgres-spilo:latest
    restart: unless-stopped
    networks:
      - twenty
    volumes:
      - twenty-db-data:/home/postgres/pgdata
    environment:
      - PGUSER_SUPERUSER=postgres
      - PGPASSWORD_SUPERUSER=your-postgres-password
      - ALLOW_NOSSL=true
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    networks:
      - twenty

3. Create .env

# .env

# App
NODE_ENV=production
SERVER_URL=https://crm.yourdomain.com

# Database
PG_DATABASE_URL=postgres://postgres:your-postgres-password@twenty-db:5432/default

# Storage
STORAGE_TYPE=local  # or 's3'

# Redis
REDIS_URL=redis://redis:6379

# Secrets — generate strong random values
APP_SECRET=$(openssl rand -hex 32)
ACCESS_TOKEN_SECRET=$(openssl rand -hex 32)
LOGIN_TOKEN_SECRET=$(openssl rand -hex 32)
REFRESH_TOKEN_SECRET=$(openssl rand -hex 32)
FILE_TOKEN_SECRET=$(openssl rand -hex 32)

# Email (optional)
EMAIL_FROM_ADDRESS=noreply@yourdomain.com
EMAIL_FROM_NAME=Twenty CRM
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASSWORD=your-smtp-password

# Auth providers (optional — defaults to email/password)
# AUTH_GOOGLE_ENABLED=true
# AUTH_GOOGLE_CLIENT_ID=your-client-id
# AUTH_GOOGLE_CLIENT_SECRET=your-secret
# AUTH_GOOGLE_CALLBACK_URL=https://crm.yourdomain.com/auth/google/redirect

4. Start Twenty

# Start the database first (migrations run automatically)
docker compose up -d

# Watch logs
docker compose logs -f twenty-server
# Look for: "Server ready at http://localhost:3000"

Configure Caddy Reverse Proxy

# /etc/caddy/Caddyfile

crm.yourdomain.com {
    reverse_proxy localhost:3000

    request_body {
        max_size 20MB
    }
}
systemctl reload caddy

Initial Setup: Create Your Workspace

Visit https://crm.yourdomain.com:

1. Click "Create account"
2. Enter your work email and set a password
3. Create your workspace:
   - Workspace name: "Acme Corp"
   - Subdomain: acme (used in workspace URLs)
4. Invite team members via email

Twenty CRM Core Concepts

Objects = Your Data Model

Twenty has standard objects and lets you create custom ones:

Standard Objects:
├── People (contacts)
├── Companies (accounts)
├── Opportunities (deals)
├── Activities (tasks, notes, calls)
└── Workspace Members (CRM users)

Custom Objects (create via Settings → Objects):
├── Customers (if you want to separate from Contacts)
├── Projects
├── Support Tickets
└── Any domain-specific entity

Creating a Custom Object

Settings → Objects → Add Custom Object
→ Name: "Partnership"
→ Fields:
   - name (text)
   - partner_company (relation → Companies)
   - revenue_share (number, %)
   - signed_date (date)
   - status (select: Active, Negotiating, Archived)
→ Save

The new object immediately appears in the left nav with table and kanban views.


Data Import

Import Contacts from CSV

People → Import → Upload CSV

Field mapping:
CSV Column        → Twenty Field
"First Name"      → First Name
"Last Name"       → Last Name
"Email"           → Email
"Company"         → Company (creates or links)
"Phone"           → Phone
"City"            → City

Import from HubSpot

1. HubSpot: Contacts → Export → CSV (all properties)
2. Twenty: People → Import → Upload CSV
3. Map HubSpot's property names to Twenty fields

For Companies:

1. HubSpot: Companies → Export → CSV
2. Twenty: Companies → Import
3. After import, re-run contacts import to link company relations

Pipeline (Opportunities)

Twenty's opportunities work like a simple Pipedrive:

Opportunities
├── Qualification
├── Demo Scheduled
├── Proposal Sent
├── Negotiation
└── Closed Won / Closed Lost

Each opportunity:
- Amount
- Close date
- Contact (many-to-many with People)
- Company
- Assignee
- Notes and activity timeline

Kanban vs Table View

View toggle: Table | Kanban | Board | Calendar

Kanban: drag opportunities between stages
Table: sort, filter, bulk edit
Calendar: close date view

Email Integration

Twenty can connect to Gmail to log emails automatically:

Settings → Integrations → Gmail
→ Connect Google account
→ Choose which emails to sync:
  - From/to specific domains
  - With specific contacts in your CRM

Emails appear in the contact/company activity timeline.


Automation (Beta)

Twenty has a workflow builder (in beta):

Workflows → New Workflow
Trigger: "When Opportunity stage changes to Closed Won"
Action:
  → Send email notification to Account Owner
  → Create task: "Send thank you gift"
  → Update People field: status = Customer

GraphQL API

Twenty exposes a full GraphQL API:

# Get all opportunities
query {
  opportunities(
    filter: { stage: { eq: "NEGOTIATION" } }
    orderBy: { amount: DescNullsLast }
  ) {
    edges {
      node {
        id
        name
        amount { amountMicros currencyCode }
        closeDate
        pointOfContactId
        company { name }
      }
    }
  }
}

# Create a new contact
mutation {
  createPerson(data: {
    name: { firstName: "Alice", lastName: "Smith" }
    emails: { primaryEmail: "alice@example.com" }
    companyId: "company-uuid"
  }) {
    id
    name { firstName lastName }
  }
}

Access the API playground at https://crm.yourdomain.com/api.


Use S3 for File Storage

For production teams with file attachments:

# .env
STORAGE_TYPE=s3
STORAGE_S3_REGION=us-east-1
STORAGE_S3_NAME=twenty-crm-files
STORAGE_S3_ENDPOINT=  # blank for AWS; set for MinIO/R2
STORAGE_S3_ACCESS_KEY_ID=your-key
STORAGE_S3_SECRET_ACCESS_KEY=your-secret

Backup

#!/bin/bash
# backup-twenty.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/twenty"

mkdir -p $BACKUP_DIR

# Database
docker compose exec -T twenty-db pg_dump \
  -U postgres default | gzip > $BACKUP_DIR/db_$DATE.sql.gz

# Local storage (attachments)
STORAGE_PATH=$(docker volume inspect twenty_twenty-server-local-data \
  --format '{{.Mountpoint}}')
tar -czf $BACKUP_DIR/storage_$DATE.tar.gz "$STORAGE_PATH"

# 30 day retention
find $BACKUP_DIR -mtime +30 -delete

echo "Twenty CRM backed up: $DATE"

Twenty vs HubSpot vs Pipedrive

FeatureTwenty (self-hosted)HubSpot (Starter)Pipedrive
Price (5 seats)~$10/mo server$450/mo$70/mo
Self-hosted
Custom objects💰 Professional+
API✅ GraphQL✅ REST✅ REST
Email sync✅ Gmail
Workflow automation⚠️ Beta
AI features
Mobile app
Data ownership✅ Full
White-label

Troubleshooting

"Database not ready" on startup:

docker compose ps twenty-db
# Wait for healthy status — the postgres-spilo image has a slower startup
# Usually resolves in 30-60 seconds

Worker not processing jobs:

docker compose logs twenty-worker | tail -20
# Common: Redis connection issues — check REDIS_URL in .env

File uploads fail:

docker compose logs twenty-server | grep "storage\|upload"
# Check STORAGE_TYPE and permissions on .local-storage volume

Twenty CRM is a top HubSpot alternative on OSSAlt — explore all open source CRM options.

Comments