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 port3000, 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 throughtwenty.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
noevictionmemory 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), withtwenty.replicasreplicas 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 whenstorage.typeislocal), 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 assembleREDIS_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
revealon 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.
- 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.
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 theuuid-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.
Configuration
The defaultvalues.yaml for this template:
Twenty Server
image— The official Twenty image. The Docker tag keeps its leadingv.replicas— Number of server replicas. Anything above1requiresstorage.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 thehttps://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 andreplicastogether 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 withyarn 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.
Attachment Storage
storage.type decides where uploaded files and attachments are kept.
- AWS S3 (keyless)
- S3-compatible (MinIO and others)
Scaling
twenty.replicas sets how many server replicas run behind the endpoint. The worker stays at one replica regardless.
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.
Access
publicAccess.enabled— Serve the UI and API on the auto-assigned*.cpln.appHTTPS endpoint (default). Set tofalsefor an internal-only instance: external requests are then refused with a403, and in-GVC callers still reach it perinternalAccess. 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 intoREDIS_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 ofpostgres (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 (usestorage.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.
- AWS S3
- Google Cloud Storage
- S3-compatible (MinIO)
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: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 installing —
secrets.namemust 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.fallbackNameset 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: falseuntil 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 existerrors 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.replicasabove1requiresstorage.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.endpointpoints at an S3-compatible server. - Change
postgres.config.password(orpostgresHA.postgres.password) andredis.auth.passwordbefore installing — both seed their component on first boot, and both are embedded in connection URLs, so use only letters, digits,-, and_. - Set
twenty.serverUrlwhen 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