Skip to main content
Deploy Twenty on Kubernetes for high availability, scalability, and production-grade infrastructure.

Overview

Kubernetes deployment provides:
  • High availability - Multiple replicas with automatic failover
  • Auto-scaling - Horizontal pod autoscaling based on load
  • Rolling updates - Zero-downtime deployments
  • Resource management - CPU and memory limits
  • Service discovery - Built-in DNS and load balancing

Prerequisites

  • Kubernetes cluster (1.24+)
  • kubectl configured
  • Helm 3 (optional, but recommended)
  • Persistent storage class configured
  • Ingress controller installed

Deployment Architecture

Kubernetes Manifests

Namespace

Create a dedicated namespace:
namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: twenty
kubectl apply -f namespace.yaml

ConfigMap

Store non-sensitive configuration:
configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: twenty-config
  namespace: twenty
data:
  NODE_ENV: "production"
  SERVER_URL: "https://crm.yourcompany.com"
  STORAGE_TYPE: "local"
  REDIS_URL: "redis://twenty-redis:6379"
  PG_DATABASE_URL: "postgres://postgres:postgres@twenty-postgres:5432/default"

Secret

Store sensitive credentials:
secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: twenty-secret
  namespace: twenty
type: Opaque
stringData:
  APP_SECRET: "your-secure-random-string"
  PG_DATABASE_PASSWORD: "your-database-password"
kubectl apply -f secret.yaml
Never commit secrets to version control. Use sealed secrets or external secret managers in production.

PostgreSQL StatefulSet

postgres.yaml
apiVersion: v1
kind: Service
metadata:
  name: twenty-postgres
  namespace: twenty
spec:
  ports:
    - port: 5432
  selector:
    app: twenty-postgres
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: twenty-postgres
  namespace: twenty
spec:
  serviceName: twenty-postgres
  replicas: 1
  selector:
    matchLabels:
      app: twenty-postgres
  template:
    metadata:
      labels:
        app: twenty-postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: "default"
            - name: POSTGRES_USER
              value: "postgres"
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: twenty-secret
                  key: PG_DATABASE_PASSWORD
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 30
            periodSeconds: 10
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

Redis Deployment

redis.yaml
apiVersion: v1
kind: Service
metadata:
  name: twenty-redis
  namespace: twenty
spec:
  ports:
    - port: 6379
  selector:
    app: twenty-redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: twenty-redis
  namespace: twenty
spec:
  replicas: 1
  selector:
    matchLabels:
      app: twenty-redis
  template:
    metadata:
      labels:
        app: twenty-redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          args:
            - --maxmemory-policy
            - noeviction
          ports:
            - containerPort: 6379
          livenessProbe:
            exec:
              command:
                - redis-cli
                - ping
            initialDelaySeconds: 15
            periodSeconds: 5

Server Deployment

server.yaml
apiVersion: v1
kind: Service
metadata:
  name: twenty-server
  namespace: twenty
spec:
  type: ClusterIP
  ports:
    - port: 3000
      targetPort: 3000
  selector:
    app: twenty-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: twenty-server
  namespace: twenty
spec:
  replicas: 2
  selector:
    matchLabels:
      app: twenty-server
  template:
    metadata:
      labels:
        app: twenty-server
    spec:
      containers:
        - name: server
          image: twentycrm/twenty:latest
          ports:
            - containerPort: 3000
          envFrom:
            - configMapRef:
                name: twenty-config
            - secretRef:
                name: twenty-secret
          volumeMounts:
            - name: storage
              mountPath: /app/packages/twenty-server/.local-storage
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: 2000m
              memory: 2Gi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 60
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 5
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: twenty-storage
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: twenty-storage
  namespace: twenty
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 20Gi

Worker Deployment

worker.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: twenty-worker
  namespace: twenty
spec:
  replicas: 2
  selector:
    matchLabels:
      app: twenty-worker
  template:
    metadata:
      labels:
        app: twenty-worker
    spec:
      containers:
        - name: worker
          image: twentycrm/twenty:latest
          command: ["yarn", "worker:prod"]
          envFrom:
            - configMapRef:
                name: twenty-config
            - secretRef:
                name: twenty-secret
          env:
            - name: DISABLE_DB_MIGRATIONS
              value: "true"
            - name: DISABLE_CRON_JOBS_REGISTRATION
              value: "true"
          volumeMounts:
            - name: storage
              mountPath: /app/packages/twenty-server/.local-storage
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: twenty-storage

Ingress

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: twenty-ingress
  namespace: twenty
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - crm.yourcompany.com
      secretName: twenty-tls
  rules:
    - host: crm.yourcompany.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: twenty-server
                port:
                  number: 3000

Deploy All Resources

Apply all manifests:
kubectl apply -f namespace.yaml
kubectl apply -f secret.yaml
kubectl apply -f configmap.yaml
kubectl apply -f postgres.yaml
kubectl apply -f redis.yaml
kubectl apply -f server.yaml
kubectl apply -f worker.yaml
kubectl apply -f ingress.yaml

Auto-Scaling

Horizontal Pod Autoscaler

Scale server pods based on CPU usage:
hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: twenty-server-hpa
  namespace: twenty
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: twenty-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
kubectl apply -f hpa.yaml

Monitoring

Health Checks

Check pod status:
kubectl get pods -n twenty
kubectl describe pod <pod-name> -n twenty

Resource Usage

kubectl top pods -n twenty
kubectl top nodes

Events

kubectl get events -n twenty --sort-by='.lastTimestamp'

Updates and Rollbacks

Update Deployment

# Update to new version
kubectl set image deployment/twenty-server \
  server=twentycrm/twenty:v0.3.0 -n twenty

# Watch rollout status
kubectl rollout status deployment/twenty-server -n twenty

Rollback

# Rollback to previous version
kubectl rollout undo deployment/twenty-server -n twenty

# Rollback to specific revision
kubectl rollout undo deployment/twenty-server --to-revision=2 -n twenty

Advanced Configuration

Network Policies

Restrict network access:
network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: twenty-network-policy
  namespace: twenty
spec:
  podSelector:
    matchLabels:
      app: twenty-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: twenty
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: twenty-postgres
    - to:
        - podSelector:
            matchLabels:
              app: twenty-redis

Pod Disruption Budget

Ensure availability during maintenance:
pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: twenty-server-pdb
  namespace: twenty
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: twenty-server

Next Steps

Configuration

Configure environment variables

Docker Compose

Simpler deployment option

Troubleshooting

Debug common issues

Monitoring

Set up monitoring and alerts

Build docs developers (and LLMs) love