Ensemble Docs
Self-Hosting

Helm Chart Reference

Values, images, ingress, secrets integration, the migration job, autoscaling, and security defaults of the Helm chart.

The application layer is deployed by a single Helm chart. This page documents its structure and the values you will set. For the environment variables that go inside *.config and secrets, see Configuration.

Chart identity and images

FieldValue
Chart namechart
Chart versionTracks the release you deploy
App versionDefault image tag for all components

Every component image defaults to tag: "", which resolves to the chart's appVersion. This couples the whole set of images to the chart version, so a given chart deploys a matching, consistent set of images. Pin a concrete chart version and tag for reproducible production deployments; avoid tracking a moving latest tag.

global.imageRegistry is prepended to every image repository. Set it to your registry (Marketplace ECR, your mirror, or Artifact Registry). A trailing slash is normalized for you.

global:
  imageRegistry: "123456789012.dkr.ecr.us-west-2.amazonaws.com/"
  imagePullSecrets: []

Components and values

Each workload (web, server, worker) shares the same value shape:

server:
  enabled: true
  replicaCount: 1
  image:
    repository: workflows/server
    tag: ""            # empty = chart appVersion
    pullPolicy: Always
  config:              # non-secret env vars, rendered onto the pod
    CLOUD_PROVIDER: aws
    # ...
  envFrom:             # attach the secret(s) here (not automatic)
    - secretRef:
        name: app-secrets
  resources:
    requests: { memory: 1Gi, cpu: 500m }
    limits:   { memory: 2Gi, cpu: 1000m }
  service:
    type: ClusterIP
    port: 80
    targetPort: 3001
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilization: 70
    targetMemoryUtilization: 80
  pdb:
    enabled: false
    minAvailable: 1

Defaults by component:

ComponenttargetPortRequests (cpu/mem)Limits (cpu/mem)HPA min-maxHPA CPU target
web3000250m / 512Mi500m / 1Gi1 - 670%
server3001500m / 1Gi1000m / 2Gi2 - 1070%
worker(no service)100m / 256Mi500m / 512Mi1 - 860%

The worker has no Service (it pulls work from Temporal rather than receiving inbound traffic) and a 120-second termination grace period so in-flight activities can drain.

Secrets integration

Secrets are never stored in the chart. Choose one of two mechanisms; both produce a Kubernetes Secret that you then attach to components via envFrom.

Install the External Secrets Operator and create a SecretStore (or ClusterSecretStore) pointing at your cloud secrets manager. Then:

externalSecrets:
  enabled: true
  secretStoreRef:
    name: aws-secrets-manager   # your SecretStore
    kind: SecretStore
  remoteRef: workflows-prod/app-secrets   # provider secret path
  refreshInterval: 1h

This renders an ExternalSecret that extracts all keys from your provider secret into a Kubernetes Secret. Example SecretStore manifests for AWS Secrets Manager, Doppler, and Vault ship in the chart's examples/ directory.

Plain chart-created secret (simple setups)

secrets:
  create: true
  # set only the keys you need

Whichever mechanism you use, the resulting Secret is not attached to pods automatically. Set server.envFrom, worker.envFrom, and (if it needs any secret) web.envFrom to reference it, and make sure the name matches. At minimum the Secret must contain PG_BASE_URL and TEMPORAL_API_KEY.

Ingress

One ingress template serves both clouds; the difference is className plus annotations.

AWS (ALB)

ingress:
  enabled: true
  className: alb
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
    alb.ingress.kubernetes.io/ssl-redirect: "443"
  hosts:
    - host: workflows.example.com
      paths:
        - { path: /api, pathType: Prefix, service: server }
        - { path: /,    pathType: Prefix, service: web }
  tls:
    enabled: true
    certificateArn: arn:aws:acm:us-west-2:123456789012:certificate/...

When certificateArn is set, the chart injects the ACM certificate annotation and the ALB terminates TLS. Requires the AWS Load Balancer Controller.

GCP (GCE)

ingress:
  enabled: true
  className: gce
  annotations:
    kubernetes.io/ingress.global-static-ip-name: workflows-ip
    networking.gke.io/managed-certificates: workflows-cert
    kubernetes.io/ingress.allow-http: "false"
  hosts:
    - host: workflows.example.com
      paths:
        - { path: /api, pathType: Prefix, service: server }
        - { path: /,    pathType: Prefix, service: web }
  tls:
    enabled: true

On GCP, TLS is a Google-managed certificate referenced by annotation, and you reserve a global static IP. There is no certificateArn and no per-host TLS secret.

Path routing sends API prefixes (/api, and others such as /mcp, /channel, /chat, /workflow in the sample overlays) to the server service and everything else to web.

Database migration job

The chart runs schema migrations automatically:

  • It is a Kubernetes Job registered as a pre-install,pre-upgrade Helm hook, so it runs and completes before the application pods roll out on every install and upgrade.
  • backoffLimit: 0 (no retries; a failure surfaces immediately and Helm rolls back) and activeDeadlineSeconds defaults to 600.
  • It reads PG_BASE_URL (via migrations.envFrom) and optional migrations.config values such as SYSTEM_DB_NAME and SEED_DATA.
migrations:
  enabled: true
  activeDeadlineSeconds: 600
  image:
    repository: workflows/migration
    tag: ""
  envFrom:
    - secretRef:
        name: app-secrets
  config:
    SYSTEM_DB_NAME: system

Because the migration is a pre-install/pre-upgrade hook (hook weight -5), it runs before the chart's main resources, including the ServiceAccount, are created. On GCP with Workload Identity, create the ServiceAccount out-of-band (set serviceAccount.create: false) so the hook job and External Secrets can use it.

Autoscaling and disruption budgets

  • HPAs use autoscaling/v2. server and web scale on CPU (and memory if a target is set). The worker scales on CPU by default, or on custom or external metrics if you set worker.autoscaling.metrics (for example, Temporal queue depth), in which case the CPU/memory targets are ignored.
  • PodDisruptionBudgets are defined for all three workloads but disabled by default. Enable pdb.enabled per component for production to protect availability during node drains.

Service account and identity

serviceAccount:
  create: true
  name: workflows-sa
  annotations:
    # AWS IRSA:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/workflows-prod-workloads-role
    # or GCP Workload Identity:
    # iam.gke.io/gcp-service-account: workflows-prod-workloads@project.iam.gserviceaccount.com

Terraform outputs the exact annotation to use (service_account_annotation).

Security defaults

The chart is hardened by default and generally should not be loosened:

  • Pods run as non-root (runAsUser: 1000, fsGroup: 1000).
  • Containers set allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, and drop all capabilities. emptyDir volumes are mounted at /tmp and /home/node/.npm to satisfy the read-only root filesystem.
  • web pods disable service account token automount.

Namespace

namespace: workflows
createNamespace: false   # set true to have the chart create it

On this page