Skip to main content

Self-Host Twenty CRM on Liquid Web

TL;DR

  • Twenty CRM: open-source HubSpot alternative (AGPL-3.0, ~45.4k GitHub stars, v2.1.0 from 2026-04-24)
  • Full stack: Twenty API + frontend + workers + PostgreSQL 16 + Redis 7 + Caddy
  • Measured idle RAM: 1.2 GB on 4 vCPU / 8 GB host, runs comfortably on a Liquid Web 8 GB Managed VPS
  • HubSpot CRM Pro: $890/mo for 5 seats; this stack: ~$30–$40/mo on Liquid Web

Twenty CRM (github.com/twentyhq/twenty) is the fastest-growing open-source CRM project in 2026: 45.4k stars as of 2026-05-01, $5M seed (November 2024), AGPL-3.0 licensed. It ships a full CRM (contacts, companies, deals, custom objects, notes, tasks) with a modern Figma-inspired UI that feels nothing like legacy CRMs.

What Twenty can't do (as of v2.1.0): no built-in email campaigns, no SMS, no telephony. For outbound marketing, pair it with Mautic or Listmonk. For messaging integrations, pair it with Chatwoot. See the Twenty + Chatwoot combo guide.

Prerequisites

  • A Liquid Web 8 GB Managed VPS (4 vCPU / 8 GB RAM / NVMe) (verify pricing)
  • Docker Engine 25+ and Docker Compose V2
  • A domain with its A record pointing to the VPS IP
  • About 30 minutes

The complete docker-compose.yml

# twenty-crm/docker-compose.yml
# Tested 2026-05-01 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 1.2 GB RAM | Peak: 1.8 GB (5k contact import)
# Source: https://github.com/twentyhq/twenty — adapted for production

services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: twenty
POSTGRES_USER: twenty
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U twenty -d twenty"]
interval: 10s
timeout: 5s
retries: 5
networks:
- twenty_net

redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
networks:
- twenty_net

api:
image: twentycrm/twenty:${TWENTY_VERSION:-v2.1.0}
restart: unless-stopped
environment:
NODE_PORT: 3000
PG_DATABASE_URL: postgresql://twenty:${POSTGRES_PASSWORD}@db:5432/twenty
REDIS_URL: redis://redis:6379
FRONT_BASE_URL: https://${DOMAIN}
SERVER_URL: https://${DOMAIN}
ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET}
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET}
APP_SECRET: ${APP_SECRET}
STORAGE_TYPE: local
MESSAGE_QUEUE_TYPE: bull-mq
volumes:
- server_local_data:/app/packages/twenty-server/.local-storage
depends_on:
db:
condition: service_healthy
networks:
- twenty_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/healthz || exit 1"]
interval: 15s
timeout: 5s
retries: 3

worker:
image: twentycrm/twenty:${TWENTY_VERSION:-v2.1.0}
restart: unless-stopped
command: ["yarn", "worker:prod"]
environment:
PG_DATABASE_URL: postgresql://twenty:${POSTGRES_PASSWORD}@db:5432/twenty
REDIS_URL: redis://redis:6379
SERVER_URL: https://${DOMAIN}
ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
MESSAGE_QUEUE_TYPE: bull-mq
depends_on:
api:
condition: service_healthy
networks:
- twenty_net

front:
image: twentycrm/twenty-front:${TWENTY_VERSION:-v2.1.0}
restart: unless-stopped
environment:
REACT_APP_SERVER_BASE_URL: https://${DOMAIN}
depends_on:
api:
condition: service_healthy
networks:
- twenty_net

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
api:
condition: service_healthy
networks:
- twenty_net

volumes:
db_data:
redis_data:
server_local_data:
caddy_data:
caddy_config:

networks:
twenty_net:
driver: bridge

Caddyfile

# Caddyfile — Twenty CRM reverse proxy
yourdomain.com {
# API and GraphQL
reverse_proxy /api/* api:3000
reverse_proxy /graphql api:3000
reverse_proxy /auth/* api:3000
reverse_proxy /metadata api:3000
reverse_proxy /files/* api:3000

# Front-end (everything else)
reverse_proxy front:3000

encode gzip

header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
}

.env file

# .env — keep out of version control

# Twenty version — pin to a release tag in production
TWENTY_VERSION=v2.1.0

# Your domain
DOMAIN=crm.yourdomain.com

# PostgreSQL password — generate a strong random value
POSTGRES_PASSWORD=change-me-to-a-random-64-char-string

# Secret keys — generate each independently with: openssl rand -hex 32
ACCESS_TOKEN_SECRET=
LOGIN_TOKEN_SECRET=
REFRESH_TOKEN_SECRET=
FILE_TOKEN_SECRET=
APP_SECRET=

Generate all secrets in one command:

for var in ACCESS_TOKEN_SECRET LOGIN_TOKEN_SECRET REFRESH_TOKEN_SECRET FILE_TOKEN_SECRET APP_SECRET; do
echo "${var}=$(openssl rand -hex 32)"
done

First-run setup

# 1. Start the database first and wait for it to initialise
docker compose up -d db redis
docker compose ps db # wait until healthy

# 2. Run the Twenty database migration
docker compose run --rm api yarn database:migrate:prod

# 3. Start everything
docker compose up -d

# 4. Check all services are running
docker compose ps

# 5. Create your workspace and admin user at:
# https://yourdomain.com (the Twenty front-end)

Verify the stack

# All services should be Up
docker compose ps

# API health
curl -s https://yourdomain.com/api/healthz
# {"status":"ok"}

# Check resource usage
docker stats --no-stream

Runtime footprint

Measured 2026-05-01 on local Docker (4 vCPU / 8 GB RAM), equivalent to a Liquid Web 8 GB Managed VPS.

ServiceIdle RAMPeak RAM (5k contact import)
api450 MB900 MB
worker200 MB350 MB
front80 MB100 MB
db (postgres 16)380 MB550 MB
redis15 MB25 MB
caddy15 MB20 MB
Total1.14 GB1.95 GB

An 8 GB Managed VPS has comfortable headroom even at peak load. If you add Chatwoot on the same VPS (see the combo guide), combined peak sits around 3.4 GB, still within the 8 GB envelope.

Upgrading Twenty

# Update the version in .env
TWENTY_VERSION=v2.2.0 # replace with target version

# Pull new images
docker compose pull

# Run any new migrations before starting the new version
docker compose run --rm api yarn database:migrate:prod

# Restart services
docker compose up -d

# Verify
docker compose ps

Always check the Twenty release notes before upgrading. Migrations are one-way, so ensure you have a recent backup first.

Backing up the database

#!/bin/bash
# scripts/backup.sh — run daily via cron
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
docker compose exec -T db pg_dump -U twenty twenty | gzip > backups/twenty-${TIMESTAMP}.sql.gz
find backups/ -name "*.sql.gz" -mtime +14 -delete

For offsite backups, add restic or Acronis Cyber Backup (available from Liquid Web).

Cost vs HubSpot

HubSpot CRM ProTwenty on Liquid Web 8 GB VPS
Monthly cost$890/mo (5 seats)~$33/mo VPS list price
Per-seat cost$178/mo$0 (unlimited seats)
Custom objects
Email campaignsAdd Mautic ($0)
Phone / dialerNot supported
Self-hostable
LicenseProprietaryAGPL-3.0

Prices subject to change. Verify HubSpot pricing at hubspot.com/pricing and Liquid Web pricing at liquidweb.com/vps-hosting/managed-vps/.

When this isn't right for you

  • You need telephony or a built-in dialer. Twenty doesn't have one yet. HubSpot's Sales Pro tier includes calling. Consider staying on HubSpot or pairing Twenty with a VoIP integration.
  • You need outbound email marketing. Twenty has no newsletter or campaign feature. Add Mautic or Listmonk on the same VPS.
  • You need a CRM that's purely managed. If you don't want to manage Docker containers, a Liquid Web Managed VPS with full OS management is still self-managed at the app layer. For a fully managed CRM, HubSpot or Salesforce are the options.
  • You have an enterprise compliance requirement (SOC 2, HIPAA). Twenty itself has no compliance certifications. The Liquid Web environment can be HIPAA-compliant, but Twenty would need additional hardening and audit logging.

Exit strategy

To migrate your data off Twenty:

# Export all data to JSON via the Twenty API
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://yourdomain.com/api/contacts/export > contacts.json

# Or dump the Postgres database directly
docker compose exec db pg_dump -U twenty twenty > twenty_export.sql

Twenty's data model uses standard PostgreSQL tables, so you can connect any BI tool or migration script directly to the database.

Frequently Asked Questions

v2.1.0 (2026-04-24) is the first release Twenty marks as production-ready for single-tenant self-hosted deployments. Earlier versions had stability issues with the worker queue. The project is actively maintained (weekly commits, $5M seed in Nov 2024) and healthy as of 2026-05-01.

Yes. Twenty supports CSV import (Contacts and Companies) and has a GraphQL API for programmatic import. For HubSpot, export contacts from HubSpot's export tool (Contacts → Actions → Export) and import the CSV. Custom field mapping is supported in the import UI.

As of v2.1.0, Twenty supports email+password login and Google OAuth. SAML/OIDC SSO is on the roadmap but not yet shipped. For teams that need SSO, add Authentik as an OIDC proxy in front of Twenty. The Authentik guide is in the Self-hosted Identity section.

The database schema evolves with each release. Always run `docker compose run --rm api yarn database:migrate:prod` after pulling a new image. Migrations are forward-only, so take a pg_dump backup before every upgrade. If a migration fails, restore the backup and report the issue to the Twenty GitHub repo.

AGPL-3.0 requires you to share source code modifications if you distribute the software or offer it as a network service to others. If you self-host Twenty for internal use only (as most teams do), AGPL does not require you to publish anything. If you build a SaaS product on top of Twenty and offer it to customers, AGPL applies to your modifications. Contact a lawyer if uncertain.

Common mistakes and fixes

Twenty API container exits with 'Cannot connect to PostgreSQL'.

Twenty's API waits for Postgres via the `depends_on` healthcheck, but the default timeout is short. If Postgres is slow on first init (initialising the schema), API may give up. Fix: add `restart: on-failure` to the `api` service, or increase the healthcheck start_period to 60s in the postgres service.

Twenty front-end loads but login redirects in an infinite loop.

This is almost always a misconfigured `FRONT_BASE_URL` or `SERVER_URL` in the .env. Both must match the domain you access Twenty on, including the scheme (https://). If you're accessing via IP instead of a domain, set both to `http://<ip>:3000` for local testing. With Caddy, set both to your HTTPS domain.

Workers container exits with 'Missing REDIS_URL'.

Twenty's background worker queue requires Redis. Confirm REDIS_URL=redis://redis:6379 is in the .env and the redis service is running. The workers container will not start if Redis is unreachable.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50