How to Self-Host Twenty CRM 2026
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
| Feature | Twenty (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.
Why Self-Host Twenty CRM?
The case for self-hosting Twenty CRM comes down to three practical factors: data ownership, cost at scale, and operational control.
Data ownership is the fundamental argument. When you use a SaaS version of any tool, your data lives on someone else's infrastructure subject to their terms of service, their security practices, and their business continuity. If the vendor raises prices, gets acquired, changes API limits, or shuts down, you're left scrambling. Self-hosting Twenty CRM means your data and configuration stay on infrastructure you control — whether that's a VPS, a bare metal server, or a home lab.
Cost at scale matters once you move beyond individual use. Most SaaS equivalents charge per user or per data volume. A self-hosted instance on a $10-20/month VPS typically costs less than per-user SaaS pricing for teams of five or more — and the cost doesn't scale linearly with usage. One well-configured server handles dozens of users for a flat monthly fee.
Operational control is the third factor. The Docker Compose configuration above exposes every setting that commercial equivalents often hide behind enterprise plans: custom networking, environment variables, storage backends, and authentication integrations. You decide when to update, how to configure backups, and what access controls to apply.
The honest tradeoff: you're responsible for updates, backups, and availability. For teams running any production workloads, this is familiar territory. For individuals, the learning curve is real but the tooling (Docker, Caddy, automated backups) is well-documented and widely supported.
Server Requirements and Sizing
Before deploying Twenty CRM, assess your server capacity against expected workload.
Minimum viable setup: A 1 vCPU, 1GB RAM VPS with 20GB SSD is sufficient for personal use or small teams. Most consumer VPS providers — Hetzner, DigitalOcean, Linode, Vultr — offer machines in this range for $5-10/month. Hetzner offers excellent price-to-performance for European and US regions.
Recommended production setup: 2 vCPUs with 4GB RAM and 40GB SSD handles most medium deployments without resource contention. This gives Twenty CRM headroom for background tasks, caching, and concurrent users while leaving capacity for other services on the same host.
Storage planning: The Docker volumes in this docker-compose.yml store all persistent Twenty CRM data. Estimate your storage growth rate early — for data-intensive tools, budget for 3-5x your initial estimate. Hetzner Cloud and Vultr both support online volume resizing without stopping your instance.
Operating system: Any modern 64-bit Linux distribution works. Ubuntu 22.04 LTS and Debian 12 are the most commonly tested configurations. Ensure Docker Engine 24.0+ and Docker Compose v2 are installed — verify with docker --version and docker compose version. Avoid Docker Desktop on production Linux servers; it adds virtualization overhead and behaves differently from Docker Engine in ways that cause subtle networking issues.
Network: Only ports 80 and 443 need to be publicly accessible when running behind a reverse proxy. Internal service ports should be bound to localhost only. A minimal UFW firewall that blocks all inbound traffic except SSH, HTTP, and HTTPS is the single most effective security measure for a self-hosted server.
Backup and Disaster Recovery
Running Twenty CRM without a tested backup strategy is an unacceptable availability risk. Docker volumes are not automatically backed up — if you delete a volume or the host fails, data is gone with no recovery path.
What to back up: The named Docker volumes containing Twenty CRM's data (database files, user uploads, application state), your docker-compose.yml and any customized configuration files, and .env files containing secrets.
Backup approach: For simple setups, stop the container, archive the volume contents, then restart. For production environments where stopping causes disruption, use filesystem snapshots or database dump commands (PostgreSQL pg_dump, SQLite .backup, MySQL mysqldump) that produce consistent backups without downtime.
For a complete automated backup workflow that ships snapshots to S3-compatible object storage, see the Restic + Rclone backup guide. Restic handles deduplication and encryption; Rclone handles multi-destination uploads. The same setup works for any Docker volume.
Backup cadence: Daily backups to remote storage are a reasonable baseline for actively used tools. Use a 30-day retention window minimum — long enough to recover from mistakes discovered weeks later. For critical data, extend to 90 days and use a secondary destination.
Restore testing: A backup that has never been restored is a backup you cannot trust. Once a month, restore your Twenty CRM backup to a separate Docker Compose stack on different ports and verify the data is intact. This catches silent backup failures, script errors, and volume permission issues before they matter in a real recovery.
Security Hardening
Self-hosting means you are responsible for Twenty CRM's security posture. The Docker Compose setup provides a functional base; production deployments need additional hardening.
Always use a reverse proxy: Never expose Twenty CRM's internal port directly to the internet. The docker-compose.yml binds to localhost; Caddy or Nginx provides HTTPS termination. Direct HTTP access transmits credentials in plaintext. A reverse proxy also centralizes TLS management, rate limiting, and access logging.
Strong credentials: Change default passwords immediately after first login. For secrets in docker-compose environment variables, generate random values with openssl rand -base64 32 rather than reusing existing passwords.
Firewall configuration:
ufw default deny incoming
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Internal service ports (databases, admin panels, internal APIs) should only be reachable from localhost or the Docker network, never directly from the internet.
Network isolation: Docker Compose named networks keep Twenty CRM's services isolated from other containers on the same host. Database containers should not share networks with containers that don't need direct database access.
VPN access for sensitive services: For internal-only tools, restricting access to a VPN adds a strong second layer. Headscale is an open source Tailscale control server that puts your self-hosted stack behind a WireGuard mesh, eliminating public internet exposure for internal tools.
Update discipline: Subscribe to Twenty CRM's GitHub releases page to receive security advisory notifications. Schedule a monthly maintenance window to pull updated images. Running outdated container images is the most common cause of self-hosted service compromises.
Troubleshooting Common Issues
Container exits immediately or won't start
Check logs first — they almost always explain the failure:
docker compose logs -f twenty-crm
Common causes: a missing required environment variable, a port already in use, or a volume permission error. Port conflicts appear as bind: address already in use. Find the conflicting process with ss -tlpn | grep PORT and either stop it or change Twenty CRM's port mapping in docker-compose.yml.
Cannot reach the web interface
Work through this checklist:
- Confirm the container is running:
docker compose ps - Test locally on the server:
curl -I http://localhost:PORT - If local access works but external doesn't, check your firewall:
ufw status - If using a reverse proxy, verify it's running and the config is valid:
caddy validate --config /etc/caddy/Caddyfile
Permission errors on volume mounts
Some containers run as a non-root user. If the Docker volume is owned by root, the container process cannot write to it. Find the volume's host path with docker volume inspect VOLUME_NAME, check the tool's documentation for its expected UID, and apply correct ownership:
chown -R 1000:1000 /var/lib/docker/volumes/your_volume/_data
High resource usage over time
Memory or CPU growing continuously usually indicates unconfigured log rotation, an unbound cache, or accumulated data needing pruning. Check current usage with docker stats twenty-crm. Add resource limits in docker-compose.yml to prevent one container from starving others. For ongoing visibility into resource trends, deploy Prometheus + Grafana or Netdata.
Data disappears after container restart
Data stored in the container's writable layer — rather than a named volume — is lost when the container is removed or recreated. This happens when the volume mount path in docker-compose.yml doesn't match where the application writes data. Verify mount paths against the tool's documentation and correct the mapping. Named volumes persist across container removal; only docker compose down -v deletes them.
Keeping Twenty CRM Updated
Twenty CRM follows a regular release cadence. Staying current matters for security patches and compatibility. The update process with Docker Compose is straightforward:
docker compose pull # Download updated images
docker compose up -d # Restart with new images
docker image prune -f # Remove old image layers (optional)
Read the changelog before major version updates. Some releases include database migrations or breaking configuration changes. For major version bumps, test in a staging environment first — run a copy of the service on different ports with the same volume data to validate the migration before touching production.
Version pinning: For stability, pin to a specific image tag in docker-compose.yml instead of latest. Update deliberately after reviewing the changelog. This trades automatic patch delivery for predictable behavior — the right call for business-critical services.
Post-update verification: After updating, confirm Twenty CRM is functioning correctly. Most services expose a /health endpoint that returns HTTP 200 — curl it from the server or monitor it with your uptime tool.