Skip to main content

Overview

Twenty is an open-source CRM — a self-hosted alternative to Salesforce, HubSpot, and Pipedrive — with customizable objects, pipelines, views, and workflows behind a REST and GraphQL API. This template deploys the Twenty server on port 3000, a background worker that runs its job queues and cron jobs, a bundled Redis, and a PostgreSQL backing store that is a single instance by default or a highly available Patroni cluster when you opt in.

Architecture

  • Twenty server — A stateful workload running the official image, serving the React front end, the REST API, and the GraphQL API on port 3000. It scales horizontally through twenty.replicas.
  • Background worker — A second workload from the same image running yarn worker:prod. It processes the BullMQ queues (entity events, webhooks, workflow triggers, file maintenance) and runs the registered cron jobs. It is always exactly one replica.
  • Redis — A bundled single-node Redis with AOF persistence and a noeviction memory policy, deployed by this chart. Twenty does not boot without it: the queues and the cache both live here, and an evicted key would silently drop queued jobs.
  • PostgreSQL (single instance, default) — The postgres template as a subchart, running PostgreSQL 18. Holds every record, workspace, and workspace-level schema.
  • PostgreSQL (highly available, optional) — The postgres-highly-available template instead, running PostgreSQL 17: 3× Patroni PostgreSQL with automatic failover, 3× etcd, and an HAProxy leader endpoint that Twenty connects through.
  • Attachment storage — Either a shared persistent volume mounted by both the server and the worker (default), or an S3 bucket you own.

What Gets Created

  • Stateful Twenty Workload — The Twenty server on port 3000 ({release}-twenty), with twenty.replicas replicas and configurable CPU and memory.
  • Standard Worker Workload — The background worker ({release}-twenty-worker), pinned to one replica, with no inbound access at all.
  • Stateful Redis Workload — The bundled single-node Redis ({release}-twenty-redis) with password authentication.
  • Database Workloads — Single mode: one stateful PostgreSQL workload. HA mode: a stateful Patroni PostgreSQL workload, a stateful etcd workload, and a standard HAProxy leader-routing workload.
  • Volume Sets — A shared (read-write-many) attachment volume set mounted by both the server and the worker at /app/packages/twenty-server/.local-storage (only when storage.type is local), one for the Redis AOF file at /data, and the database subchart’s data volumes.
  • Secrets — A dictionary secret ({release}-twenty-creds) holding the bundled Redis password used to assemble REDIS_URL, and an opaque secret ({release}-twenty-worker-start) holding the worker’s start script. Your prerequisite app-key secret is not created by the template — see Prerequisites.
  • Identity & Policy — One identity shared by the server, worker, and Redis workloads, and a policy granting it reveal on exactly the secrets they read: the credentials secret, the worker start script, the active database’s credentials secret, your app-key secret, and — when configured — the rotation fallback and S3 static-key secrets. In keyless S3 mode the identity also carries the cloud account link and your bucket-scoped IAM policy.
  • Cron Backup Workload (optional) — Created inside the backing PostgreSQL store when database backups are enabled.
This template does not create a GVC. You must deploy it into an existing GVC.

Prerequisites

Twenty encrypts stored OAuth tokens, TOTP secrets, and app variables, and signs its auth tokens, with a single app key that you supply through an opaque secret created before installing. The value is never passed through Helm values.
1

Create the app key secret

The payload is a single random string:
Use printf rather than echo so the payload carries no trailing newline.
2

Point the template at it

Set secrets.name to the secret’s name (default my-twenty-app-secret). Secrets are org-level, so no GVC flag is involved. The template supplies the value to Twenty as both APP_SECRET and ENCRYPTION_KEY.
3

Back the value up

Store a copy somewhere safe, outside Control Plane.
Create the secret before installing. Without it the deployment sits waiting on a secret that does not exist and never becomes ready, which looks like a broken install. Treat the key as write-once: changing secrets.name without also setting secrets.fallbackName to the previous key makes stored OAuth tokens, TOTP secrets, and app variables undecryptable and logs everyone out. See Rotating the App Key.
Everything else works with the defaults. Two optional features need their own setup first:
  • S3 attachment storage — an existing bucket plus either a Control Plane cloud account (AWS) or a static-key secret (S3-compatible servers). See Attachment Storage. Required if you plan to run more than one replica.
  • Database backups — a bucket and provider access set up beforehand. See Backing Up.
Once your app key secret exists, install the template using your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Choosing a Database Mode

Exactly one of the two backing stores must be enabled — the chart enforces this at render. Twenty is wired to the active database automatically, including for its boot migrations, and both modes create the uuid-ossp and unaccent extensions on first boot. The two paths run different PostgreSQL majors, so pick the mode before you have data: moving an existing database between them is a pg_dump and restore, not a values change. To use HA mode, set postgres.enabled: false and postgresHA.enabled: true.

First Run

Twenty ships no default account, and the first person to sign up creates the workspace and becomes its full administrator.
1

Wait out the first boot

PostgreSQL and Redis come up first, then Twenty creates its schemas, installs the uuid-ossp and unaccent extensions, and runs its migrations before it starts serving. Expect roughly 4 minutes on the single-instance database and roughly 9 minutes in HA mode, where the server retries against the HAProxy endpoint until Patroni has elected a leader.
2

Ignore the first minute of log noise

Every fresh install logs error: relation "core.appToken" does not exist and similar lines for about a minute, along with [DatabaseConfigDriver] [INIT] Failed to load config variables from database, falling back to environment variables. These are containers reading the schema while it is still being created; they stop on their own once migrations finish.
3

Sign up immediately

Browse to the canonical *.cpln.app endpoint of the {release}-twenty workload and create the first account. It becomes the workspace administrator with full admin-panel access.
4

Finish configuration inside the app

SMTP, AI provider keys, rate limits, and OAuth/SSO are not template values. Set them in Settings → Admin Panel → Configuration Variables; changes take effect within about 15 seconds.
Do not abort a slow first install. The Twenty server does not listen on port 3000 until its boot migrations complete, and in HA mode the container deliberately restarts while it waits for the Patroni leader. Give it the full window above before investigating.

Configuration

The default values.yaml for this template:

Twenty Server

  • image — The official Twenty image. The Docker tag keeps its leading v.
  • replicas — Number of server replicas. Anything above 1 requires storage.type: s3; the chart fails the render with a clear message otherwise. See Scaling.
  • serverUrl — The public base URL Twenty uses as its front-end origin and for auth callbacks, including the https:// scheme. Leave it empty to use the workload’s canonical endpoint; set it when you serve Twenty on a custom domain. A value that does not match the URL browsers actually use breaks authentication and CORS with opaque errors.
  • dbPoolMaxConnections — PostgreSQL connections per pool, per process. Raise it and replicas together with care: the database has a finite connection limit, and the worker holds its own pools.
  • resources — CPU and memory bounds for the server container. The memory ceiling leaves headroom for the first-boot migrations and upgrade backfills.

Background Worker

The worker runs the same image with yarn worker:prod and is always exactly one replica — it registers the repeatable cron jobs, and it owns the boot migrations whenever twenty.replicas is above 1. There is no worker.replicas knob; only worker.resources is configurable. The worker exposes no port and accepts no inbound traffic. Records created in the UI enqueue jobs that it drains: with the worker stopped, jobs pile up in Redis and complete as soon as it comes back.

Rotating the App Key

secrets.name points at the opaque secret holding the current app key. secrets.fallbackName points at a second opaque secret holding the previous key, and exists only for the duration of a rotation — with it set, rows encrypted under the old key stay readable while new writes use the new key.
1

Create the new key secret

2

Upgrade with both names set

Set secrets.name to the new secret and secrets.fallbackName to the old one. The identity is granted reveal on exactly those two secrets, plus the ones the template creates.
3

Let the rollout finish

The server re-runs its boot migrations during this rollout, so it takes about as long as a first install. Sessions issued under the old key keep working, and existing records stay readable.
Never change secrets.name without setting secrets.fallbackName to the previous key. Stored OAuth tokens, TOTP secrets, and app variables are encrypted with the old value and become undecryptable, and every user is logged out.

Attachment Storage

storage.type decides where uploaded files and attachments are kept.
Attachments are written to a shared (read-write-many) volume set mounted at /app/packages/twenty-server/.local-storage by both the server and the worker, so the worker’s file-maintenance jobs see what the server uploaded, and vice versa. Files survive restarts, redeploys, and upgrades under the same release name.
Shared volume sets support expansion only — they cannot be snapshotted, and they exist in a single location. For production, storage.type: s3 is the durable choice. Local storage also works only with a single replica; the chart refuses to render twenty.replicas above 1 with it.

Scaling

twenty.replicas sets how many server replicas run behind the endpoint. The worker stays at one replica regardless.
Boot migrations must run in exactly one container, and the template places them automatically — you do not configure this:
Because the worker owns migrations at two or more replicas, a cold multi-replica install serves errors for a minute or two while the worker creates the schema — the servers are up before the database is ready for them. This affects first installs only; upgrades of an existing release have a populated schema already.
At two replicas, a rolling upgrade served 508 of 508 health checks successfully with no failures, and force-stopping one replica served 198 of 198 requests while the replacement came back — the rollout keeps at most one replica out at a time.
twenty.replicas above 1 requires storage.type: s3. The chart refuses to render the combination with local storage.

Access

  • publicAccess.enabled — Serve the UI and API on the auto-assigned *.cpln.app HTTPS endpoint (default). Set to false for an internal-only instance: external requests are then refused with a 403, and in-GVC callers still reach it per internalAccess. Firewall changes take a couple of minutes to propagate.
  • internalAccess.type — Controls which workloads can reach Twenty over the internal network:
The bundled Redis and the database are never publicly reachable; they accept in-GVC traffic only.

Redis

The bundled Redis carries Twenty’s BullMQ job queues and its cache, and Twenty does not boot without it.
  • redis.image — The Redis image to run.
  • redis.auth.password — Password for the bundled Redis, wired into REDIS_URL. Change it before installing. Use only letters, digits, -, and _: the value is embedded in a URL. Authentication is required — an unauthenticated or wrong-password client is refused.
  • redis.resources / redis.volumeset.capacity — CPU and memory bounds and the volume size for the AOF file at /data. Queue state is restored from the AOF after a restart.
Redis runs with maxmemory-policy noeviction and appendonly yes, and this is deliberate: an evicted key would silently drop queued jobs. Around a Redis restart the server logs connection-reset errors and recovers on its own.

Database

Enable exactly one of postgres (single instance, default) or postgresHA (highly available) — see Choosing a Database Mode. In both modes, change the database password before installing (postgres.config.password / postgresHA.postgres.password); it seeds the database on first boot and cannot be changed by editing values afterwards. Use only letters, digits, -, and _ — the password is embedded in the connection URL. Twenty connects to the single instance directly, or to the HAProxy leader endpoint in HA mode, and creates the uuid-ossp and unaccent extensions itself on first boot. postgres.resources / postgresHA.resources and the volumeset.capacity values (GiB, minimum 10, per replica in HA mode) size the backing store.

Connecting

Backing Up

Database backups are optional and disabled by default. When enabled, a scheduled job runs inside the backing PostgreSQL store and uploads to your bucket under the configured prefix — covering every record, workspace, and user, but not the attachments on the storage volume set (use storage.type: s3 for durable attachments). Enable with postgres.backup.enabled or postgresHA.backup.enabled, matching your database mode, and complete the storage setup for your provider before installing. The values below are shown under backup.* — set them within the enabled database block.
1

Create a bucket

Create an S3 bucket. Set backup.aws.bucket and backup.aws.region to match.
2

Set up a Cloud Account

If you do not have one, create a Cloud Account for your AWS account. Set backup.aws.cloudAccountName to its name.
3

Create a bucket-scoped IAM policy

Create an IAM policy granting the required S3 actions on the bucket, and set backup.aws.policyName to its name:
In HA mode, postgresHA.backup.mode selects logical (scheduled pg_dump) or wal-g (continuous WAL archiving). The single-instance path takes a postgres.backup.schedule cron expression. The full per-provider walkthrough, including restores, lives in the backing postgres and postgres-highly-available template documentation.

Important Notes

  • Create the app key secret before installingsecrets.name must point at an existing opaque secret (plain encoding). Without it the deployment waits on a secret that does not exist and looks broken.
  • The app key is effectively write-once — rotate it only with secrets.fallbackName set to the previous key, or stored OAuth tokens, TOTP secrets, and app variables become undecryptable and everyone is logged out.
  • Sign up immediately after install — there is no seeded account, and on a public endpoint the first visitor to sign up becomes the workspace administrator. Set publicAccess.enabled: false until you are ready if that is a concern.
  • First boot is slow and noisy, and that is expected — about 4 minutes on the single-instance database and about 9 in HA mode, with relation "core.…" does not exist errors for roughly the first minute. Do not abort the install.
  • The single-instance and HA paths run different PostgreSQL majors — 18 and 17 respectively. Choose before you have data; switching is a dump and restore.
  • twenty.replicas above 1 requires storage.type: s3 — the shared local volume set lives in one location and cannot be snapshotted, so S3 is the production recommendation regardless of replica count.
  • AWS S3 is keyless only — use a cloud account plus a bucket-scoped IAM policy. Static keys are accepted only when storage.s3.endpoint points at an S3-compatible server.
  • Change postgres.config.password (or postgresHA.postgres.password) and redis.auth.password before installing — both seed their component on first boot, and both are embedded in connection URLs, so use only letters, digits, -, and _.
  • Set twenty.serverUrl when Twenty sits behind a custom domain, with the scheme (https://crm.example.com). A mismatch with the URL browsers use breaks auth callbacks and CORS with opaque errors.
  • SMTP, AI provider keys, rate limits, and OAuth/SSO are not values knobs — configure them in Settings → Admin Panel → Configuration Variables inside the app.
  • Foreign data wrappers (“remote objects”) are unavailable — that feature needs upstream’s custom PostgreSQL image, which this template does not deploy.
  • Data survives restarts and upgrades; uninstalling deletes it — records live in the database volume set and local attachments in the storage volume set, and both go with the release. Your prerequisite app key secret is yours and survives an uninstall.
  • Twenty is licensed under the AGPL — this template deploys the open-source edition.

External References

Twenty Documentation

Official Twenty documentation

Self-Hosting Setup

Configuration variables and the in-app admin panel

Upgrade Guide

Moving an existing deployment to a newer release

REST and GraphQL API

Query and mutate CRM records over HTTP

Twenty on GitHub

Source code and release notes

Twenty Template

View the source files, default values, and chart definition