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 (React front end plus NestJS API on port 3000), a background worker that drains its job queues and runs its cron jobs, a bundled single-node Redis, and a PostgreSQL database that is a single instance by default or a highly available Patroni cluster with one flag. Attachments go to a shared persistent volume or to an S3 bucket you own. Twenty is AGPL-3.0 licensed; the template runs the unmodified upstream image.
This template does not create a GVC. It deploys into an existing GVC that you already have; pass it with --gvc GVC_NAME.

What Gets Created

Your app key secret (see Prerequisites) is not created by the template.

Prerequisites

Twenty encrypts stored OAuth tokens, TOTP secrets and app variables, and signs its auth tokens, with one app key that you supply through an opaque secret created before installing. The value never passes through Helm values; the template hands it to Twenty as both APP_SECRET and ENCRYPTION_KEY.
1

Create the app key secret

The payload is a single random string. Secrets are org-level, so no --gvc flag is involved:
Set secrets.name to this name.
2

Back the value up

Store a copy outside Control Plane. The key is effectively write-once: changing secrets.name without pointing secrets.fallbackName at the previous key makes stored OAuth tokens, TOTP secrets and app variables undecryptable and logs everyone out. See Rotating the App Key.
3

Choose the two bundled passwords

The database password (postgres.credentials.password) and the Redis password (redis.auth.password) ship as change-me-… placeholders that the template uses as-is. Pick strong values and pass them at install time. Both are embedded in connection URLs, so use only letters, digits, - and _; the chart refuses to render anything else. The database password seeds the database on first boot and is not changed by later value edits.
If the app key secret does not exist at install time, the deployment wedges silently. cpln logs returns zero lines — the container never starts, so it has nothing to log. The one place the reason appears is status.versions[].message:
Note this is get-deployments — plain cpln workload get has no versions field. Creating the secret repairs the deployment on its own after several minutes, or force a redeployment to skip the wait.
Everything else works with the defaults. Three optional features need their own setup first:
  • S3 attachment storage — an existing bucket plus either a Control Plane cloud account (AWS S3) or a static-key dictionary secret (S3-compatible servers). See Attachment Storage. Required for more than one server replica.
  • Database backups — a bucket and access setup for AWS S3, Google Cloud Storage or an S3-compatible server. See Backing Up.
  • App key rotation — a second opaque secret holding the previous key, referenced by secrets.fallbackName for the duration of the rotation. See Rotating the App Key.

Installation

Once the app key secret exists, install into your existing GVC with the two bundled passwords set:
To install using another method, follow the instructions for it:

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

Configuration

Each subsection shows the shipped defaults for one top-level values area.

Twenty Server

Both the server and the worker run twenty.image. Leave twenty.serverUrl empty on a public install: the server then advertises its own canonical endpoint, and the worker advertises the server’s — never its own. Set it, with the scheme, whenever browsers open Twenty at any other address: a custom domain (https://crm.example.com), or the address you reach through a port-forward on a private install (http://localhost:3000). Twenty compares the browser origin against SERVER_URL, so a mismatch breaks sign-in and CORS with opaque errors.

Background Worker

The worker drains the job queues (entity events, webhooks, workflow triggers, file maintenance) and runs the registered cron jobs. It is a fixed single replica with no worker.replicas knob, exposes no port and accepts no inbound traffic. It has no probes, so it reports ready as soon as it is scheduled — read its logs rather than its readiness:

App Key Secret

secrets.name is the prerequisite opaque secret from Prerequisites. secrets.fallbackName is set only while rotating the key — see Rotating the App Key.

Attachment Storage

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. Files survive restarts, redeploys and upgrades under the same release name.
Shared volume sets support expansion only — they cannot be snapshotted — and exist in a single location. For production, storage.type: s3 is the durable choice, and it is required for twenty.replicas above 1; the chart refuses to render that combination with local storage.

Access

Public access is on by default. Set publicAccess.enabled: false for an internal-only instance — external requests are then refused, and in-GVC callers still reach it according to internalAccess.type: The worker and the bundled Redis are never publicly reachable: the worker accepts no inbound traffic, and Redis accepts only same-GVC traffic. A change to either access knob takes a few minutes to take effect.

Redis

Redis is required — Twenty does not boot without REDIS_URL — and it runs as a single node with persistence on and eviction disabled, so a queued job is never dropped. Twenty accepts only a plain redis:// URL, which is why the Sentinel-based redis template is not used here. Change redis.auth.password before installing; only letters, digits, - and _ are accepted because the value is embedded in a connection URL.

Database

Enable exactly one of postgres (single instance, default) and postgresHA (highly available) — the chart refuses to render with both or neither. Twenty is wired to the active database automatically: the single instance directly, or the HAProxy leader endpoint in HA mode. In both modes the database credentials come from postgres.credentials.*: this template writes them into a dictionary secret named by postgres.config.credentialsSecretName or postgresHA.config.credentialsSecretName and hands that name to the bundled database. There is nothing for you to create.
The other keys of the bundled postgres and postgres-highly-available templates are available under postgres.* and postgresHA.*, with one exception: postgresHA.proxy.enabled must stay true, because the HAProxy endpoint is the address Twenty connects to.
Choose the database mode before you have data. The two paths run different PostgreSQL majors (18 and 17) on separate volume sets, so flipping postgres.enabled / postgresHA.enabled on a live release points Twenty at a different, empty database — your existing records are orphaned, not migrated. Moving between the modes is a dump and restore.
Secret names are org-wide, and the credentials secret is created by this template in both modes. If you run more than one release of this template in the same org, give each its own postgres.config.credentialsSecretName (or postgresHA.config.credentialsSecretName). A second release left on the default name is refused at install with cannot be updated because it is being managed by a different release and creates nothing; the first release is unaffected.

Connecting

Read the public hostname and the stored credentials:
To verify the server from your machine without going through the public endpoint, forward port 3000 and query the health route:
On an install with publicAccess.enabled: false, the same port-forward is how you reach the UI in a browser at http://localhost:3000 — and twenty.serverUrl must then be set to that address, because Twenty checks the browser origin against it.

First Run

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

Wait for the server workload to report ready

A first install takes several minutes: the database and Redis come up first, then the server waits for a live PostgreSQL, runs its boot migrations and only then starts listening on port 3000. Every fresh install logs relation "core.appToken" does not exist and similar lines early in the boot — the migration framework probing for tables it has not created yet — and the readiness probe may report a transient failure during the migration window. Both stop on their own once migrations finish; do not abort the install.
2

Sign up immediately

Browse to the canonical endpoint of RELEASE_NAME-twenty and create the first account. On a public endpoint, whoever reaches the URL first owns the CRM — sign up as soon as the workload is ready, or install with publicAccess.enabled: false and reach the sign-up page through a port-forward (set twenty.serverUrl to http://localhost:3000 for that session).
3

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; they are stored in the database and apply without a redeploy.

Operations

Backing Up

Database backups are optional and off by default. They cover the PostgreSQL database — every record, workspace and user. Attachments are not included: in S3 mode they live in your bucket, in local mode on the RELEASE_NAME-twenty-storage volume set, which cannot be snapshotted. Enable backups with postgres.backup.enabled: true or postgresHA.backup.enabled: true (matching your database mode) and complete the storage setup for your provider before installing. The keys below are shown as backup.*; set them inside 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 AWS IAM policy with the same bucket-scoped JSON shown under Attachment Storage (replace YOUR_BUCKET_NAME with the backup bucket), then set backup.aws.policyName to the policy’s name.
Single-instance mode takes scheduled logical dumps through the RELEASE_NAME-postgres-backup cron workload on backup.schedule. In HA mode, backup.mode selects logical (a scheduled dump run by RELEASE_NAME-postgres-ha-backup) or wal-g (continuous WAL archiving every backup.walg.intervalSeconds). backup.*.prefix is the key prefix within the bucket; schedules are cron expressions in UTC.

Restoring a Backup

The database restore is the one documented for the bundled database: Restoring a Backup on the postgres page, or Restoring a Backup on the postgres-highly-available page (logical and WAL-G). In HA mode, connect through RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local so the restore targets the current leader; use the username and password from the secret named by *.config.credentialsSecretName. Three Twenty-specific points:
  • The server and worker must not be writing to the database while a dump is replayed. After the restore, force a redeployment of both so they reconnect:
  • Attachments are restored separately: copy them back into the bucket (S3 mode) or onto the RELEASE_NAME-twenty-storage volume set (local mode).
  • Keep the app key from your prerequisite secret with the backup. A dump restored under a different ENCRYPTION_KEY cannot decrypt the OAuth tokens, TOTP secrets and app variables it contains.
This restore path has not been exercised against a Twenty install. The database procedure is the bundled template’s own; rehearse it on a test release before you depend on it.

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 a rotation: Twenty uses it as FALLBACK_ENCRYPTION_KEY, a verification-only key that keeps rows encrypted under the old key readable while new writes use the new key.
1

Create the new key secret

2

Upgrade with both names set

Run helm upgrade with secrets.name pointing at the new secret and secrets.fallbackName at 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. Twenty’s documentation does not say that rows are re-encrypted under the new key, so do not clear secrets.fallbackName on an assumption — keep both secrets until you have confirmed the old key is no longer needed.
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.

Upgrading From 1.0.0

Version 1.0.1 only updates the bundled highly available database to a release that compacts its etcd cluster. Without compaction, etcd’s backend grows with time alone and eventually goes read-only, taking PostgreSQL failover with it. Only installs running the HA database (postgresHA.enabled: true, off by default here) are affected — see etcd History Compaction for the mechanism and the symptoms. Compaction stops further growth but cannot shrink a backend that has already grown.

Upgrading From 1.0.1 or Earlier

Version 1.1.0 changed the single-instance database path:
  • postgres.config.username, postgres.config.password and postgres.config.database moved to postgres.credentials.username, postgres.credentials.password and postgres.credentials.database. The new postgres.config.credentialsSecretName names the dictionary secret this template now creates from those three values. Carrying the old keys fails the render with config.username was REMOVED in postgres 3.4.0 — move the three keys and you are done, and ignore that message’s advice to create a secret yourself.
  • postgres.backup.minio.accessKey and postgres.backup.minio.secretKey were replaced by postgres.backup.minio.credentialsSecretName, a prerequisite dictionary secret holding accessKey and secretKey — see Backing Up.
Keep postgres.credentials.* equal to the values your database was initialised with; the running database still enforces them.

Upgrading From 1.1.0 or Earlier

Version 1.2.0 moved the highly available database to postgres-highly-available 2.5.0, which no longer creates its own credentials secret:
  • postgresHA.postgres.username, postgresHA.postgres.password and postgresHA.postgres.database were removed. Carrying them fails the render with the postgres block was REMOVED in 2.5.0. Delete the block.
  • In HA mode the credentials now come from postgres.credentials.* — the same keys the single-instance path uses — and this template writes them into the secret named by the new postgresHA.config.credentialsSecretName.
Use the credentials your cluster already has. Set postgres.credentials.username, postgres.credentials.password and postgres.credentials.database to exactly the values your postgresHA.postgres.* block held. The database was initialised with them and still enforces them; putting new values in the secret does not change the running database — it gives Twenty a password that no longer works.

Upgrading From 1.2.0 or Earlier

Version 1.2.1 replaced postgresHA.backup.minio.accessKey and postgresHA.backup.minio.secretKey with postgresHA.backup.minio.credentialsSecretName, a prerequisite dictionary secret holding accessKey and secretKey. Only installs backing up the HA database to an S3-compatible server are affected — see Backing Up for the secret. Version 1.3.0 changes no values. It makes the server and worker wait for a live PostgreSQL before running boot migrations, which fixes a first-install failure where the containers could start against a database that was not yet accepting connections.

Scaling and Availability

  • Server tier — twenty.replicas sets how many server replicas run behind the endpoint. Anything above 1 requires storage.type: s3; the chart refuses to render the combination with local storage.
  • Boot migrations run in exactly one container, and the template places them automatically: Because the worker owns migrations above one replica, a fresh multi-replica install serves errors until the worker has created the schema. Upgrades of an existing release already have a populated schema.
  • Worker and Redis — both are fixed at one replica by design: the worker owns cron registration, and Twenty accepts only a plain redis:// URL. A Redis restart stalls background jobs briefly; the UI stays up.
  • Database — the opt-in postgresHA path keeps serving through a replica failure and a rolling restart; the default postgres path has no failover. The mode is an install-time choice.
  • Helm upgrades — the first helm upgrade after an install can restart the bundled database and Redis even when their values did not change; upgrade in a quiet window.

Troubleshooting

Symptom: RELEASE_NAME-twenty and RELEASE_NAME-twenty-worker never reach ready, and cpln logs returns zero lines.Cause: a secret referenced by name does not exist — usually the app key secret (secrets.name), or the fallback or S3 key secret when those are set. The container never starts, so there is nothing to log.Fix: read status.versions[].message, which names the missing secret, then create it. The deployment recovers on its own after several minutes, or force a redeployment to skip the wait.
Symptom: a fresh install logs error: relation "core.appToken" does not exist and similar lines, and the readiness probe reports a transient failure.Cause: the migration framework probes for its state tables before creating them. This is expected on every fresh install and stops once the boot migrations finish.Fix: none — wait for the server to report ready. If the server never does, read its log for a migration error:
Symptom: the server or worker log repeats twenty: waiting for Postgres... (attempt N) and ends with twenty: Postgres not ready after 250s; exiting for restart.Cause: the start script waits for the database to answer a real PostgreSQL handshake before running migrations, and the database was not accepting connections within its window. On a first HA install this can happen once while the cluster elects a leader; a database that never comes up is a different problem.Fix: the container restarts and retries on its own. If it keeps happening, check the database workload — RELEASE_NAME-postgres or RELEASE_NAME-postgres-ha — with cpln workload get-deployments and its logs.
Symptom: the UI loads but sign-in loops or fails, and the browser console shows CORS or origin errors.Cause: the address in the browser does not match SERVER_URL. This happens with a custom domain, or when reaching a private install through a port-forward, while twenty.serverUrl is empty.Fix: set twenty.serverUrl to exactly the URL browsers use, including the scheme (https://crm.example.com, or http://localhost:3000 for a port-forward), and run helm upgrade.
Symptom: helm install or helm upgrade fails before anything is applied, citing twenty.replicas > 1 requires storage.type: s3.Cause: local attachments live on a volume set, which does not serve more than one server replica.Fix: complete the S3 setup under Attachment Storage and set storage.type: s3, or keep twenty.replicas: 1.
Symptom: the render fails citing static keys (storage.s3.auth.secretName) are only for S3-compatible servers.Cause: storage.s3.auth.secretName is set but storage.s3.endpoint is empty. AWS S3 is keyless only.Fix: for AWS S3, clear storage.s3.auth.secretName and use storage.s3.cloudAccountName plus storage.s3.policyName; for an S3-compatible server, set storage.s3.endpoint.
Symptom: helm upgrade fails before anything is applied, citing config.username was REMOVED in postgres 3.4.0.Cause: your values still carry the 1.0.x single-instance keys postgres.config.username, postgres.config.password or postgres.config.database.Fix: move the three keys to postgres.credentials.* as described in Upgrading From 1.0.1 or Earlier. Do not create a secret yourself — this template creates it.
Symptom: helm upgrade fails before anything is applied, citing the postgres block was REMOVED in 2.5.0.Cause: your values still carry postgresHA.postgres.username, postgresHA.postgres.password or postgresHA.postgres.database from version 1.1.0 or earlier.Fix: delete the postgresHA.postgres block and put the same three values under postgres.credentials.* — see Upgrading From 1.1.0 or Earlier.
Symptom: installing a second Twenty release in the same org fails with cannot be updated because it is being managed by a different release, and nothing is created.Cause: both releases use the same credentialsSecretName. Secret names are org-wide, and the first release owns that secret.Fix: give the second release its own postgres.config.credentialsSecretName (or postgresHA.config.credentialsSecretName). The first release is unaffected.
Symptom: the render fails citing the database password must contain only letters, digits, '-' and '_' or the same message for redis.auth.password.Cause: both passwords are embedded in connection URLs, and other characters break the URL.Fix: choose a password made of letters, digits, - and _ only. For the database password this must be done before the first install — it seeds the database and is not changed by later value edits.

Important Notes

  • Create the app key secret before installing — secrets.name must point at an existing opaque secret (plain encoding). Without it the deployment wedges silently; see Prerequisites.
  • 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. Install with publicAccess.enabled: false until you are ready if that is a concern.
  • Set twenty.serverUrl whenever browsers open Twenty at anything other than its canonical endpoint — a custom domain or a port-forward — with the scheme. A mismatch breaks sign-in and CORS with opaque errors.
  • Change postgres.credentials.password and redis.auth.password before installing — both are embedded in connection URLs, so use only letters, digits, - and _; the database password seeds the database on first boot and is not changed by later value edits.
  • Choose the database mode before you have data — the single-instance and HA paths run PostgreSQL 18 and 17 respectively on separate volume sets; switching is a dump and restore, not a values change.
  • twenty.replicas above 1 requires storage.type: s3 — the shared local volume set cannot be snapshotted and lives in one location, 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.
  • Give each release its own credentialsSecretName — secret names are org-wide, and a second release on the default name is refused at install.
  • 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.

External References

Twenty Documentation

Official Twenty documentation

Self-Hosting Setup

Configuration variables and the in-app admin panel

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