Skip to content

lifecycle

Version: 0.9.12 Type: application AppVersion: 0.2.1

A Helm umbrella chart for full Lifecycle stack

Installation

Prerequisites

Create a minimal values.yaml file with your domain configuration:

global:
  domain: YOUR-DOMAIN.com  # CHANGE THIS to your actual domain
  image:
    tag: 0.1.3

distribution:
  ingress:
    hostname: distribution.YOUR-DOMAIN.com  # CHANGE THIS to match your domain

buildkit:
  buildkitdToml: |
    debug = true
    [registry."distribution.YOUR-DOMAIN.com"]  # CHANGE THIS to match your domain
      http = true
      insecure = true
    [worker.oci]
      platforms = [ "linux/amd64" ]
      reservedSpace = "60%"
      maxUsedSpace = "80%"
      max-parallelism = 25

Important: Replace all instances of YOUR-DOMAIN.com with your actual domain name.

Install the Chart

helm upgrade -i lifecycle \
  oci://ghcr.io/goodrxoss/helm-charts/lifecycle \
  --version 0.9.12 \
  -f values.yaml \
  -n lifecycle-app \
  --create-namespace

Lifecycle API Keycloak Credentials

The bundled Keycloak chart creates two separate service-account credentials. The Lifecycle web Deployment receives only lifecycle-api-keycloak-management; the worker receives only lifecycle-api-principal-sync. Neither credential is exposed through a shared envFrom, the gateway, or Lifecycle UI. The Keycloak server pod receives neither secret; the realm-import Job reads both through KeycloakRealmImport placeholders to substitute them into the two clients at first import.

KeycloakRealmImport creates both clients only when the realm is first imported. It does not reconcile an existing realm. For an existing installation, create a missing lifecycle-api-keycloak-management confidential service-account client externally with direct assignments only for manage-clients and manage-realm, and make its secret match the configured Kubernetes Secret before enabling MCP. Lifecycle API owns all feature-specific Keycloak configuration.

The chart preserves the distinguishing Secret-name suffixes for long release names and rejects any configuration that resolves both credentials to the same Secret key. External Secrets may hold both credentials under distinct keys. Chart-generated credential Secrets use helm.sh/resource-policy: keep, so uninstalling the release does not remove them. If the Keycloak realm is retained, revoke or delete the matching Keycloak clients before deleting the retained Secrets. Rotate a credential by updating the Keycloak client and Kubernetes Secret as one coordinated operation, then restart only its owning Deployment (web for management, worker for principal sync). Because realm import is one-shot, changing a Helm value alone does not rotate an existing Keycloak client.

Requirements

Repository Name Version
https://andrcuns.github.io/charts buildkit(buildkit-service) 1.4.0
https://charts.bitnami.com/bitnami minio(minio) 17.0.21
https://charts.bitnami.com/bitnami postgres(postgresql) 15.5.19
https://charts.bitnami.com/bitnami redis(redis) 19.6.3
https://goodrxoss.github.io/helm-charts keycloak(lifecycle-keycloak) 0.7.6
https://goodrxoss.github.io/helm-charts ui(lifecycle-ui) 0.3.5
https://jouve.github.io/charts distribution(distribution) 0.1.7

Values

Key Type Default Description
buildkit.buildkitdToml string "debug = true\n[registry.\"distribution.example.com\"]\n http = true\n insecure = true\n[worker.oci]\n platforms = [ \"linux/amd64\" ]\n reservedSpace = \"60%\"\n maxUsedSpace = \"80%\"\n max-parallelism = 25\n"
buildkit.enabled bool true
buildkit.fullnameOverride string ""
buildkit.resources.requests.cpu string "500m"
buildkit.resources.requests.memory string "1Gi"
components.gateway.container.args list []
components.gateway.container.command list []
components.gateway.deployment.affinity object {}
components.gateway.deployment.envFrom object {}
components.gateway.deployment.extraEnv[0].name string "LIFECYCLE_MODE"
components.gateway.deployment.extraEnv[0].value string "gateway"
components.gateway.deployment.livenessProbe.failureThreshold int 6
components.gateway.deployment.livenessProbe.httpGet.path string "/api/health"
components.gateway.deployment.livenessProbe.httpGet.port string "http"
components.gateway.deployment.livenessProbe.initialDelaySeconds int 60
components.gateway.deployment.livenessProbe.periodSeconds int 10
components.gateway.deployment.nodeSelector object {}
components.gateway.deployment.ports[0].containerPort int 80
components.gateway.deployment.ports[0].name string "http"
components.gateway.deployment.ports[0].protocol string "TCP"
components.gateway.deployment.readinessProbe.failureThreshold int 3
components.gateway.deployment.readinessProbe.httpGet.path string "/api/health"
components.gateway.deployment.readinessProbe.httpGet.port string "http"
components.gateway.deployment.readinessProbe.periodSeconds int 5
components.gateway.deployment.replicaCount int 2
components.gateway.deployment.resources.limits.cpu string "2000m"
components.gateway.deployment.resources.limits.ephemeral-storage string "1Gi"
components.gateway.deployment.resources.limits.memory string "4000Mi"
components.gateway.deployment.resources.requests.cpu string "200m"
components.gateway.deployment.resources.requests.ephemeral-storage string "400Mi"
components.gateway.deployment.resources.requests.memory string "512Mi"
components.gateway.deployment.startupProbe object {}
components.gateway.deployment.tolerations list []
components.gateway.deployment.volumeMounts list []
components.gateway.deployment.volumes list []
components.gateway.enabled bool false
components.gateway.extraLabels object {}
components.gateway.fullnameOverride string ""
components.gateway.hpa.enabled bool false
components.gateway.hpa.maxReplicas int 10
components.gateway.hpa.metrics[0].resource.name string "cpu"
components.gateway.hpa.metrics[0].resource.target.averageUtilization int 80
components.gateway.hpa.metrics[0].resource.target.type string "Utilization"
components.gateway.hpa.metrics[0].type string "Resource"
components.gateway.hpa.minReplicas int 2
components.gateway.ingress.annotations object {}
components.gateway.ingress.defaultBackend object {}
components.gateway.ingress.enabled bool false
components.gateway.ingress.hosts[0].host string "*.sites.example.com"
components.gateway.ingress.hosts[0].paths[0] string "/"
components.gateway.ingress.ingressClassName string "nginx"
components.gateway.networkPolicy.egress list []
components.gateway.networkPolicy.enabled bool false
components.gateway.networkPolicy.ingress list []
components.gateway.pdb.enabled bool false
components.gateway.pdb.minAvailable int 1
components.gateway.service.clusterIP string ""
components.gateway.service.enabled bool true
components.gateway.service.externalTrafficPolicy string ""
components.gateway.service.port int 80
components.gateway.service.targetPort int 80
components.gateway.service.type string "ClusterIP"
components.web.container.args list []
components.web.container.command list []
components.web.deployment.affinity object {}
components.web.deployment.envFrom object {}
components.web.deployment.extraEnv[0].name string "LIFECYCLE_MODE"
components.web.deployment.extraEnv[0].value string "web"
components.web.deployment.livenessProbe.failureThreshold int 6
components.web.deployment.livenessProbe.httpGet.path string "/api/health"
components.web.deployment.livenessProbe.httpGet.port string "http"
components.web.deployment.livenessProbe.initialDelaySeconds int 60
components.web.deployment.livenessProbe.periodSeconds int 10
components.web.deployment.nodeSelector object {}
components.web.deployment.ports[0].containerPort int 80
components.web.deployment.ports[0].name string "http"
components.web.deployment.ports[0].protocol string "TCP"
components.web.deployment.readinessProbe.failureThreshold int 3
components.web.deployment.readinessProbe.httpGet.path string "/api/health"
components.web.deployment.readinessProbe.httpGet.port string "http"
components.web.deployment.readinessProbe.periodSeconds int 5
components.web.deployment.replicaCount int 1
components.web.deployment.resources.requests.cpu string "200m"
components.web.deployment.resources.requests.memory string "200Mi"
components.web.deployment.startupProbe object {}
components.web.deployment.tolerations list []
components.web.deployment.volumeMounts list []
components.web.deployment.volumes list []
components.web.enabled bool true
components.web.extraLabels object {}
components.web.fullnameOverride string ""
components.web.ingress.annotations."cert-manager.io/cluster-issuer" string "letsencrypt"
components.web.ingress.defaultBackend object {}
components.web.ingress.enabled bool true
components.web.ingress.hosts list []
components.web.ingress.ingressClassName string "nginx"
components.web.service.enabled bool true
components.web.service.port int 80
components.web.service.targetPort int 80
components.web.service.type string "ClusterIP"
components.worker.container.args list []
components.worker.container.command list []
components.worker.deployment.affinity object {}
components.worker.deployment.envFrom object {}
components.worker.deployment.extraEnv[0].name string "LIFECYCLE_MODE"
components.worker.deployment.extraEnv[0].value string "job"
components.worker.deployment.extraEnv[1].name string "STAGE"
components.worker.deployment.extraEnv[1].value string "prod"
components.worker.deployment.extraEnv[2].name string "LOG_LEVEL"
components.worker.deployment.extraEnv[2].value string "info"
components.worker.deployment.extraEnv[3].name string "MAX_GITHUB_API_REQUEST"
components.worker.deployment.extraEnv[3].value string "33"
components.worker.deployment.extraEnv[4].name string "GITHUB_API_REQUEST_INTERVAL"
components.worker.deployment.extraEnv[4].value string "10000"
components.worker.deployment.livenessProbe.failureThreshold int 12
components.worker.deployment.livenessProbe.httpGet.path string "/api/health"
components.worker.deployment.livenessProbe.httpGet.port string "http"
components.worker.deployment.livenessProbe.initialDelaySeconds int 60
components.worker.deployment.livenessProbe.periodSeconds int 10
components.worker.deployment.livenessProbe.timeoutSeconds int 10
components.worker.deployment.nodeSelector object {}
components.worker.deployment.ports[0].containerPort int 80
components.worker.deployment.ports[0].name string "http"
components.worker.deployment.ports[0].protocol string "TCP"
components.worker.deployment.readinessProbe.failureThreshold int 3
components.worker.deployment.readinessProbe.httpGet.path string "/api/jobs"
components.worker.deployment.readinessProbe.httpGet.port string "http"
components.worker.deployment.readinessProbe.periodSeconds int 5
components.worker.deployment.replicaCount int 1
components.worker.deployment.resources.limits.cpu string "1000m"
components.worker.deployment.resources.limits.memory string "4000Mi"
components.worker.deployment.resources.requests.cpu string "200m"
components.worker.deployment.resources.requests.memory string "200Mi"
components.worker.deployment.startupProbe object {}
components.worker.deployment.tolerations list []
components.worker.deployment.volumeMounts list []
components.worker.deployment.volumes list []
components.worker.enabled bool true
components.worker.extraLabels object {}
components.worker.fullnameOverride string ""
components.worker.hpa.enabled bool false
components.worker.hpa.maxReplicas int 5
components.worker.hpa.metrics[0].resource.name string "cpu"
components.worker.hpa.metrics[0].resource.target.averageUtilization int 80
components.worker.hpa.metrics[0].resource.target.type string "Utilization"
components.worker.hpa.metrics[0].type string "Resource"
components.worker.hpa.minReplicas int 1
components.worker.ingress.enabled bool false
components.worker.networkPolicy.egress list []
components.worker.networkPolicy.enabled bool false
components.worker.networkPolicy.ingress list []
components.worker.pdb.enabled bool false
components.worker.pdb.minAvailable int 1
components.worker.service.clusterIP string ""
components.worker.service.enabled bool false
components.worker.service.externalTrafficPolicy string ""
config.annotations list []
config.enabled bool true
config.fullnameOverride string ""
distribution.enabled bool true
distribution.image.tag string "2.8.3"
distribution.ingress.annotations."nginx.ingress.kubernetes.io/proxy-body-size" string "0"
distribution.ingress.enabled bool true
distribution.ingress.hostname string "distribution.example.com"
distribution.ingress.ingressClassName string "nginx"
distribution.persistence.enabled bool true
distribution.persistence.size string "20Gi"
externalDatabase.database string "lifecycle"
externalDatabase.enabled bool false
externalDatabase.host string nil
externalDatabase.password.secretKeyRef.key string nil
externalDatabase.password.secretKeyRef.name string nil
externalDatabase.port int 5432
externalDatabase.username string "lifecycle"
global.appSubDomain string "app"
global.domain string "example.com"
global.envFrom object {}
global.env[0].name string "JOB_VERSION"
global.env[0].value string "default"
global.env[1].name string "ENVIRONMENT"
global.env[1].value string "production"
global.env[2].name string "NODE_ENV"
global.env[2].value string "production"
global.env[3].name string "APP_ENV"
global.env[3].value string "production"
global.env[4].name string "PINO_PRETTY"
global.env[4].value string "false"
global.env[5].name string "PORT"
global.env[5].value string "80"
global.extraEnv list []
global.extraLabels object {}
global.image.pullPolicy string "IfNotPresent"
global.image.repository string "lifecycleoss/app"
global.image.tag string ""
global.imagePullSecrets list []
global.podSecurityContext.fsGroup int 2000
global.scope string "core"
global.securityContext.readOnlyRootFilesystem bool false
global.securityContext.runAsNonRoot bool false
global.serviceAccount.annotations list []
global.serviceAccount.create bool true
global.serviceAccount.name string ""
global.uiSubDomain string "ui"
keycloak.clients.lifecycleApiKeycloakManagement.clientId string "lifecycle-api-keycloak-management" Keycloak client ID for Lifecycle web.
keycloak.clients.lifecycleApiKeycloakManagement.clientSecret.secretKeyRef.key string nil
keycloak.clients.lifecycleApiKeycloakManagement.clientSecret.secretKeyRef.name string nil
keycloak.clients.lifecycleApiKeycloakManagement.enabled bool true Inject the Keycloak-management credential only into Lifecycle web.
keycloak.clients.lifecycleApiPrincipalSync.clientId string "lifecycle-api-principal-sync" Keycloak client ID for Lifecycle worker principal synchronization.
keycloak.clients.lifecycleApiPrincipalSync.clientSecret.secretKeyRef.key string nil
keycloak.clients.lifecycleApiPrincipalSync.clientSecret.secretKeyRef.name string nil
keycloak.clients.lifecycleApiPrincipalSync.enabled bool true Inject the read-only principal-sync credential only into Lifecycle worker.
keycloak.clients.lifecycleUi.url string "https://ui.example.com"
keycloak.enabled bool true
keycloak.externalDatabase.database string "keycloak"
keycloak.externalDatabase.enabled bool false
keycloak.externalDatabase.host string nil
keycloak.externalDatabase.password.secretKeyRef.key string nil
keycloak.externalDatabase.password.secretKeyRef.name string nil
keycloak.externalDatabase.port int 5432
keycloak.externalDatabase.username string "keycloak"
keycloak.externalDatabase.vendor string "postgres"
keycloak.githubIdp.clientId.secretKeyRef.key string "GITHUB_CLIENT_ID"
keycloak.githubIdp.clientId.secretKeyRef.name string "{{ include \"lifecycle-keycloak.parentChartPrefix\" . }}-bootstrap"
keycloak.githubIdp.clientSecret.secretKeyRef.key string "GITHUB_CLIENT_SECRET"
keycloak.githubIdp.clientSecret.secretKeyRef.name string "{{ include \"lifecycle-keycloak.parentChartPrefix\" . }}-bootstrap"
keycloak.githubIdp.defaultScope string "repo user:email"
keycloak.githubIdp.githubJsonFormat bool true
keycloak.hostname string "https://auth.example.com"
keycloak.keycloakPostgres.enabled bool true
keycloak.keycloakPostgres.nameOverride string "keycloak-postgres"
keycloak.secrets.apiKeycloakManagement.clientSecret string nil
keycloak.secrets.apiKeycloakManagement.enabled bool true Create the lookup-stable Keycloak-management client Secret.
keycloak.secrets.apiKeycloakManagement.fullnameOverride string ""
keycloak.secrets.apiPrincipalSync.clientSecret string nil
keycloak.secrets.apiPrincipalSync.enabled bool true Create the lookup-stable principal-sync client Secret.
keycloak.secrets.apiPrincipalSync.fullnameOverride string ""
keycloak.secrets.postgres.enabled bool true
minio.auth.existingSecret string "{{ include \"..helper.objectStoreSecretName\" . }}"
minio.auth.rootPasswordSecretKey string "OBJECT_STORE_SECRET_KEY"
minio.auth.rootUserSecretKey string "OBJECT_STORE_ACCESS_KEY"
minio.clientImage.repository string "bitnamilegacy/minio-client"
minio.clientImage.tag string "2025.7.23"
minio.console.image.repository string "bitnamilegacy/minio-object-browser"
minio.console.image.tag string "2.0.2-debian-12-r3"
minio.defaultBuckets string "lifecycle-logs"
minio.enabled bool false
minio.fullnameOverride string ""
minio.image.repository string "bitnamilegacy/minio"
minio.image.tag string "2025.7.23"
minio.persistence.enabled bool true
minio.persistence.size string "20Gi"
minio.resources.requests.cpu string "100m"
minio.resources.requests.memory string "256Mi"
minio.volumePermissions.image.repository string "bitnamilegacy/os-shell"
minio.volumePermissions.image.tag string "2025.7.23"
objectStore.bucket string ""
objectStore.endpoint string ""
objectStore.port string "9000"
objectStore.region string ""
objectStore.type string "minio"
objectStore.useSSL string "false"
postgres.auth.database string "lifecycle"
postgres.auth.existingSecret string "{{ include \"..helper.postgresSecretName\" . }}"
postgres.auth.secretKeys.adminPasswordKey string "POSTGRES_ADMIN_PASSWORD"
postgres.auth.secretKeys.userPasswordKey string "POSTGRES_USER_PASSWORD"
postgres.auth.username string "lifecycle"
postgres.enabled bool true
postgres.fullnameOverride string ""
postgres.image.repository string "bitnamilegacy/postgresql"
postgres.primary.persistence.enabled bool true
postgres.primary.persistence.size string "11Gi"
rbac.create bool true
redis.architecture string "standalone"
redis.auth.enabled bool true
redis.auth.existingSecret string "{{ include \"..helper.redisSecretName\" . }}"
redis.auth.existingSecretPasswordKey string "REDIS_PASSWORD"
redis.auth.password string "password"
redis.enabled bool true
redis.fullnameOverride string ""
redis.image.repository string "bitnamilegacy/redis"
redis.master.persistence.enabled bool true
redis.master.persistence.size string "8Gi"
secrets.bootstrap.annotations list []
secrets.bootstrap.enabled bool true
secrets.bootstrap.fullnameOverride string ""
secrets.bootstrap.githubAppId string ""
secrets.bootstrap.githubClientId string ""
secrets.bootstrap.githubClientSecret string ""
secrets.bootstrap.githubInstallationId string ""
secrets.bootstrap.githubPrivateKey string ""
secrets.bootstrap.githubWebhookSecret string ""
secrets.common.annotations list []
secrets.common.enabled bool true
secrets.common.encryptionKey string ""
secrets.common.fullnameOverride string ""
secrets.objectStore.accessKey string ""
secrets.objectStore.annotations list []
secrets.objectStore.enabled bool true
secrets.objectStore.fullnameOverride string ""
secrets.objectStore.secretKey string ""
secrets.postgres.annotations list []
secrets.postgres.enabled bool true
secrets.postgres.fullnameOverride string ""
secrets.postgres.postgresAdminPassword string ""
secrets.postgres.postgresUserPassword string ""
secrets.redis.annotations list []
secrets.redis.enabled bool true
secrets.redis.fullnameOverride string ""
secrets.redis.redisPassword string ""
ui.config.apiUrl string "https://app.example.com"
ui.config.appUrl string "" Public UI URL used for links in API responses; defaults to https://..
ui.config.authBaseUrl string "https://app.example.com"
ui.config.authClientId string "lifecycle-ui"
ui.config.authClientSecret.secretKeyRef.key string "clientSecret"
ui.config.authClientSecret.secretKeyRef.name string "{{ include \"lifecycle-ui.parentChartPrefix\" . }}-keycloak-lifecycle-ui"
ui.config.authRealm string "lifecycle"
ui.enabled bool true
ui.nameOverride string "ui"