Skip to content

How to Deploy Twenty CRM on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Aug 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Twenty on your own VPS with the official Docker Compose stack — a modern, open-source CRM and self-hosted alternative to Salesforce, up and running behind generated secrets in under an hour.

Before you start
  • A VPS with 2 vCPU / 4 GB RAM — Twenty's published minimum is 2 GB, but that number covers a single container; the real stack is server, worker, Postgres, and Redis running together
  • A fresh Ubuntu 24.04/26.04 server with root/sudo SSH access, Docker Engine + the Compose plugin installed (see Docker & Compose on Ubuntu)
  • A domain you can point at the server if you want real HTTPS rather than testing over a bare IP
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Twenty is

Twenty is an open-source CRM built around a modern, Notion-like interface — contacts, companies, deals, and fully customizable pipelines, with a GraphQL API and a Postgres-backed data model you can extend without writing code. It's positioned squarely as an open alternative to Salesforce: the same core object model (people, companies, opportunities) without the seat-based pricing or the vendor lock-in.

The appeal of self-hosting it is the usual one: your customer data lives on infrastructure you own, and the cost is the box, not a per-seat SaaS bill that grows with headcount. Twenty is under active development and licensed AGPL-3.0 — running an unmodified instance for your own team carries no extra obligations, but if you fork it and offer it as a hosted service to others, AGPL's source-disclosure terms kick in.

This guide uses Twenty's official Docker Compose stack straight from the twentyhq/twenty repository — the supported, documented install path — rather than a hand-rolled compose file.

Server sizing — why 4 GB, not 2

Twenty's published minimum is 2 GB of RAM, and it's easy to read that as "a 2 GB box is enough." It isn't, in practice, because Twenty isn't one process — the official compose stack brings up four services together: the server (the API and the web UI it serves), a worker that handles everything asynchronous — background jobs, workflow automation, imports, email sync — Postgres as the system of record, and Redis as the queue backend the worker reads from. Uploaded files and attachments land on local disk by default too, which needs headroom of its own as your data grows.

Four services sharing 2 GB leaves very little slack, and a Node-based server plus a Node-based worker are not light processes at idle, let alone under load. 2 vCPU / 4 GB RAM is the realistic floor for a small team's instance with room to breathe. That's why we recommend a 2 vCPU / 4 GB VPS. If your team is larger than a handful of people or you're importing a big existing contact/company database, size up to 8 GB — it's cheaper than fighting OOM kills on the worker mid-import.

Prepare the server

This guide assumes you've already worked through Docker & Compose on Ubuntu — a fresh Ubuntu 24.04 or 26.04 box, Docker Engine and the Compose plugin installed, and ufw allowing SSH plus 80 and 443. Twenty's server listens on port 3000 by default; you don't need to open it to the internet — the plan below is to proxy it through Caddy on 443, the same pattern as HTTPS for any other Docker app on this site.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Twenty

Twenty publishes an official docker-compose.yml and a matching .env.example in the twenty-docker package of its repo. Pull both down, then generate the two secrets the compose file needs and append them to .env:

mkdir twenty && cd twenty
wget -O docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml
wget -O .env https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/.env.example
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '\n')" >> .env

A couple of things worth understanding about those two values before you move on:

  • PG_DATABASE_PASSWORD is the password Twenty's Postgres container is initialized with, and the one the server and worker use to reach it. It only matters on first boot — change it later and the running database won't match it anymore. Use openssl rand -hex, not -base64, here: Twenty's own .env.example warns this password must have no special characters, and base64 output can include + and /, which break the connection string the server builds — the failure mode is ugly (a psql: invalid integer value ... for connection option "port" loop) and only shows up on the unlucky passwords that happen to contain one of those characters.
  • ENCRYPTION_KEY is the key Twenty uses to encrypt sensitive data at rest (tokens, secrets stored in the app) and to sign sessions. Treat it like a root password: back up .env somewhere safe right now. Lose this key and encrypted data becomes unrecoverable; change it later on a running instance and existing sessions and encrypted values break.

.env.example ships more variables than just those two — most are fine at their defaults for a first deploy, but set SERVER_URL to the address you'll actually reach the app on before you go live (https://crm.example.com once you've wired up the domain below, or http://SERVER_IP:3000 if you're only testing). Twenty uses it to build links, API URLs, and outbound email content, and the default only works for local testing.

Bring the stack up:

docker compose up -d || true

The server runs ~180 database migrations against Postgres on first boot, and that routinely takes longer than the server's own healthcheck retry budget — so this first docker compose up -d commonly exits with dependency failed to start: container twenty-server-1 is unhealthy, and the worker container never starts. That's expected, not a failed deploy (the || true keeps the command from erroring out): the server keeps retrying in the background (its compose entry sets restart: always) and finishes the migrations on its own. Watch it, then start the worker once it reports healthy:

until docker compose ps server | grep -q '(healthy)'; do sleep 5; done
docker compose up -d --no-recreate

Use --no-recreate so the second up starts the missing worker without bouncing the already-healthy server — a plain up -d can recreate the server and reset its healthcheck clock, failing the same way again on a warm first boot. You should see db, redis, server, and worker all Up. Once the server's log settles, it's reachable at http://SERVER_IP:3000.

HTTPS + domain

Testing over http://SERVER_IP:3000 is fine for a first look, but a CRM holding real customer data should be served over HTTPS on a real domain — plain HTTP means credentials and session cookies cross the network in the clear. Twenty's official compose file doesn't bundle a reverse proxy, so add one.

The simplest path is Automatic HTTPS with Caddy, which fetches and renews Let's Encrypt certificates for you with a couple of lines of config. Point an A record for your hostname (say crm.example.com) at the server's public IP, then have Caddy reverse-proxy that hostname to 127.0.0.1:3000. Once DNS resolves, Caddy issues a certificate automatically and Twenty is live on HTTPS.

After DNS and Caddy are both working, go back and set SERVER_URL in .env to your real https://crm.example.com address if you hadn't already, then restart so the server picks it up:

docker compose up -d

A mismatched SERVER_URL is the single most common misconfiguration on a first deploy — get it wrong and links in emails, the API base URL, and file download links all point at the wrong place.

First-run setup

Load https://crm.example.com (or http://SERVER_IP:3000 if you're still testing) in a browser. Twenty shows a sign-up screen on a fresh install.

Create your account and workspace immediately. As with most self-hosted tools, the sign-up screen is open to anyone who can reach it until the first account exists — whoever signs up first owns the workspace. Enter your email, set a strong password, and name your workspace.

Once you're in, do a quick smoke test to confirm the whole stack is actually wired together correctly:

  1. Create a Company record and a Person attached to it — this exercises the core data model and confirms writes are landing in Postgres.
  2. Open the Opportunities (or Deals) view and drag a card between pipeline stages — this is a good check that the UI and API round-trip cleanly.
  3. Upload a file to a record (an avatar or an attachment) and reload the page — this confirms the worker and local file storage are both working, not just the server.

If all three work, the server, worker, Postgres, and Redis are all doing their job.

Backups

Two things matter here, and losing either one hurts:

  • The Postgres data — every contact, company, deal, and pipeline you've built lives in the db service's volume. This is the actual content of your CRM.
  • .env — specifically ENCRYPTION_KEY. Restore a Postgres backup onto an instance running a different encryption key and anything Twenty encrypted at rest becomes unreadable, even though the raw rows are intact.

Dump the database and copy it, plus .env, off the box:

docker compose exec -T db pg_dump -U postgres default | gzip > twenty-db-$(date +%F).sql.gz

(Confirm the exact database/user names for your install with docker compose exec db env | grep PG_ if you changed anything from the .env.example defaults.) Also snapshot the storage volume that holds uploaded files and attachments — check docker compose config for the volume name if you're not sure which one it is.

Get all of it — the SQL dump, .env, and the storage volume — to an S3-compatible bucket or another machine on a schedule. A backup that only lives on the same server dies with the server, and an untested backup is a guess: periodically restore into a throwaway stack to confirm it actually works.

Upgrades

Twenty ships new releases regularly. To upgrade, pull newer images and recreate the containers:

docker compose pull
docker compose up -d

The server runs pending database migrations automatically on boot, which is exactly why a fresh backup before you pull is non-negotiable — a migration changes your data in place and there's no undo without one. Read the release notes on the Twenty GitHub releases page before a major version bump, and consider pinning specific image tags in docker-compose.yml instead of riding latest, so you control exactly when you move and can reproduce the running version if something breaks.

Troubleshooting

Server container restarts in a loop right after docker compose up -d. Usually it started before Postgres was ready to accept connections. Check docker compose logs db for startup errors first, then docker compose logs server — a docker compose up -d retry once Postgres settles is often enough.

The app loads but API calls fail, or links in the UI point at the wrong host. SERVER_URL doesn't match the address you're actually serving from. Set it to your real public HTTPS URL in .env and restart the stack — this is the same class of bug as a wrong WEBHOOK_URL in other self-hosted apps: the app has to know its own public address, it can't infer it from the request.

Uploaded files or avatars 404 after restarting the stack. The local storage volume isn't being persisted — confirm the compose file's storage volume mount survived any edits you made, and that you're not accidentally running with docker compose down -v, which deletes volumes.

Login sessions keep getting invalidated, or previously-working sessions break after a redeploy. ENCRYPTION_KEY changed between deploys. It must stay identical across restarts and upgrades — restore it from your backup if it drifted.

Certificate never issues / stuck on plain HTTP. This is a Caddy/DNS problem, not a Twenty one — confirm the A record for your domain has propagated (dig +short crm.example.com) and that port 80 is reachable from the internet for the ACME challenge to complete.

Verification + next steps

You're done when you can: load Twenty over HTTPS on your own domain, sign in as the account you created, create and move records through a pipeline, upload a file and see it persist across a restart, and confirm a database dump plus .env are landing in off-box storage.

From here, invite your team, import existing contacts and companies, and build out the pipeline stages that match how you actually sell. The box underneath it is what makes or breaks the experience once real usage starts — 2 vCPU / 4 GB is the right tier to start. See Best VPS for CRM for the current options ranked by price.

Next steps

How to self-host Twenty →More self-hosted crm tools →Best VPS for a CRM →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.