Skip to content

Metabase

Open-source business intelligence and analytics platform. Metabase lets teams build interactive dashboards, run SQL queries, and share insights without writing code. It stores all metadata (dashboards, saved questions, user accounts, data source credentials) in a PostgreSQL database.

Key Features

  • Interactive dashboards — drag-and-drop visualization builder, no SQL required
  • SQL editor — native query editor with autocomplete and result visualization
  • PostgreSQL metadata store — bundled subchart or external database
  • Encrypted credential storage — data source passwords encrypted with encryptionSecretKey
  • JVM tuning — configurable Java memory options and timezone
  • S3 backup — scheduled pg_dump of the Metabase metadata database to S3-compatible storage
  • Ingress support — TLS via cert-manager with configurable ingress class
  • Gateway API support — optional HTTPRoute for Kubernetes-native traffic routing
  • Dual-stack ready Service — optional ipFamilyPolicy and ipFamilies
  • External Secrets support — ESO integration for the Metabase encryption key

Installation

HTTPS repository:

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install metabase helmforge/metabase

OCI registry:

helm install metabase oci://ghcr.io/helmforgedev/helm/metabase

Deployment Examples

# values.yaml — Metabase with bundled PostgreSQL subchart (default)
metabase:
  siteUrl: 'https://metabase.example.com'
  encryptionSecretKey: 'a-random-32-char-secret-key-here'
  javaOpts: '-Xms512m -Xmx1g'

postgresql:
  enabled: true
  auth:
    password: 'postgres-password'

ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - host: metabase.example.com
      paths:
        - path: /
          pathType: Prefix
# values.yaml — Metabase pointing to an existing PostgreSQL instance
metabase:
  siteUrl: 'https://metabase.example.com'
  encryptionSecretKey: 'a-random-32-char-secret-key-here'

postgresql:
  enabled: false

database:
  external:
    host: postgresql.database.svc
    port: '5432'
    name: metabase
    username: metabase
    password: 'db-password'

ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - host: metabase.example.com
      paths:
        - path: /
          pathType: Prefix
# values.yaml — Daily backup of the Metabase PostgreSQL metadata database
metabase:
  siteUrl: 'https://metabase.example.com'
  encryptionSecretKey: 'a-random-32-char-secret-key-here'

postgresql:
  enabled: true
  auth:
    password: 'postgres-password'

backup:
  enabled: true
  schedule: '0 3 * * *'
  s3:
    endpoint: https://s3.amazonaws.com
    bucket: my-metabase-backups
    accessKey: '<set-me>'
    secretKey: '<set-me>'
# values.yaml — Production-grade setup with secrets, resources, and TLS
metabase:
  siteUrl: 'https://metabase.example.com'
  existingSecret: metabase-secret
  existingSecretKey: encryption-secret-key
  javaOpts: '-Xms1g -Xmx2g'
  javaTimezone: America/Sao_Paulo

postgresql:
  enabled: false

database:
  external:
    host: postgresql.production.svc
    port: '5432'
    name: metabase
    username: metabase
    existingSecret: metabase-db-secret
    existingSecretPasswordKey: password

resources:
  requests:
    memory: 1.5Gi
    cpu: 500m
  limits:
    memory: 2.5Gi
    cpu: 2000m

ingress:
  enabled: true
  ingressClassName: traefik
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: metabase.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: metabase-tls
      hosts:
        - metabase.example.com

Configuration Reference

Core

ParameterTypeDefaultDescription
nameOverridestring""Override the chart name.
fullnameOverridestring""Override the full release name.
commonLabelsobject{}Extra labels added to all resources.

Image

ParameterTypeDefaultDescription
image.repositorystringdocker.io/metabase/metabaseMetabase container image.
image.tagstring"v0.60.4"Image tag.
image.pullPolicystringIfNotPresentImage pull policy.
imagePullSecretsarray[]Pull secrets for private registries.

Metabase Configuration

ParameterTypeDefaultDescription
metabase.portinteger3000Internal HTTP port for the Metabase container.
metabase.encryptionSecretKeystring""Secret key used to encrypt saved data source credentials.
metabase.existingSecretstring""Existing secret containing the encryption key.
metabase.existingSecretKeystringencryption-secret-keyKey inside the existing secret holding the encryption value.
metabase.siteUrlstring""Public URL of the Metabase instance. Used for email links and embeds.
metabase.aiFeaturesEnabledbooleanfalseEnable Metabase AI features after configuring a supported provider.
metabase.javaTimezonestringUTCJVM timezone (TZ environment variable).
metabase.javaOptsstring""JVM options for memory tuning (e.g. -Xms512m -Xmx1g).
metabase.extraEnvarray[]Extra environment variables injected into the Metabase container.
Always set encryptionSecretKey

If metabase.encryptionSecretKey is empty, Metabase auto-generates one at startup. If the pod is recreated without a persistent key, all saved data source credentials become unreadable and must be re-entered manually. Always set an explicit key or use metabase.existingSecret.

JVM memory sizing

Metabase is a JVM application. Without -Xmx, the JVM may claim all available container memory. Set metabase.javaOpts: '-Xms512m -Xmx1g' and resources.limits.memory to a value at least 20% higher than -Xmx to avoid OOMKilled restarts.

Database — Embedded Subchart

ParameterTypeDefaultDescription
postgresql.enabledbooleantrueDeploy a bundled PostgreSQL subchart for Metabase metadata.
postgresql.architecturestringstandalonePostgreSQL deployment architecture.
postgresql.auth.databasestringmetabaseDatabase name created by the subchart.
postgresql.auth.usernamestringmetabaseDatabase username created by the subchart.
postgresql.auth.passwordstring""Database password (auto-generated if empty).

Database — External

ParameterTypeDefaultDescription
database.external.hoststring""External PostgreSQL hostname or IP.
database.external.portstring"5432"External PostgreSQL port.
database.external.namestringmetabaseDatabase name on the external server.
database.external.usernamestringmetabaseUsername for the external database.
database.external.passwordstring""Password for the external database (plain text — prefer secret).
database.external.existingSecretstring""Existing secret containing the database password.
database.external.existingSecretPasswordKeystringpasswordKey inside the existing secret for the password.

Service

ParameterTypeDefaultDescription
service.typestringClusterIPKubernetes service type.
service.portinteger80Service port exposed to the cluster.
service.annotationsobject{}Annotations for the Service.
service.ipFamilyPolicystring/nullnullService IP family policy.
service.ipFamiliesarray[]Ordered Service IP families.

Dual-Stack Service

service:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

Ingress

ParameterTypeDefaultDescription
ingress.enabledbooleanfalseEnable an Ingress resource.
ingress.ingressClassNamestringtraefikIngress class name.
ingress.annotationsobject{}Annotations for the Ingress (e.g. cert-manager).
ingress.hostsarray[]Ingress host and path rules.
ingress.tlsarray[]TLS configuration (secret name and hosts).

Gateway API

Set gatewayAPI.enabled to render a Kubernetes Gateway API HTTPRoute for Metabase. The chart expects an existing Gateway and does not create shared Gateway infrastructure.

gatewayAPI:
  enabled: true
  gatewayName: shared-gateway
  gatewayNamespace: gateway-system
  hostnames:
    - metabase.example.com
ParameterTypeDefaultDescription
gatewayAPI.enabledbooleanfalseRender a Gateway API HTTPRoute.
gatewayAPI.gatewayNamestring""Existing Gateway name, required when enabled.
gatewayAPI.gatewayNamespacestring""Namespace of the parent Gateway.
gatewayAPI.hostnamesarray[]HTTPRoute hostnames.
gatewayAPI.pathstring/HTTPRoute path match value.
gatewayAPI.pathTypestringPathPrefixHTTPRoute path match type.

The older gateway block remains supported as a compatibility alias.

Probes

Metabase has a slow startup — the JVM initialization and database migrations can take 2–3 minutes.

ParameterTypeDefaultDescription
probes.startup.enabledbooleantrueEnable startup probe (uses /api/health).
probes.startup.initialDelaySecondsinteger30Startup probe initial delay.
probes.startup.periodSecondsinteger10Startup probe period.
probes.startup.timeoutSecondsinteger5Startup probe timeout.
probes.startup.failureThresholdinteger30Startup probe failure threshold (5 minutes).
probes.liveness.enabledbooleantrueEnable liveness probe.
probes.liveness.initialDelaySecondsinteger0Liveness probe initial delay.
probes.liveness.periodSecondsinteger15Liveness probe period.
probes.liveness.timeoutSecondsinteger5Liveness probe timeout.
probes.liveness.failureThresholdinteger3Liveness probe failure threshold.
probes.readiness.enabledbooleantrueEnable readiness probe.
probes.readiness.initialDelaySecondsinteger0Readiness probe initial delay.
probes.readiness.periodSecondsinteger10Readiness probe period.
probes.readiness.timeoutSecondsinteger5Readiness probe timeout.
probes.readiness.failureThresholdinteger3Readiness probe failure threshold.

Backup

The backup CronJob runs pg_dump against the Metabase PostgreSQL metadata database and uploads the archive to S3-compatible storage. This protects dashboards, saved questions, users, and data source definitions.

ParameterTypeDefaultDescription
backup.enabledbooleanfalseEnable scheduled S3 backup CronJob.
backup.schedulestring"0 3 * * *"Cron schedule for backups.
backup.suspendbooleanfalseSuspend the CronJob without deleting it.
backup.concurrencyPolicystringForbidCronJob concurrency policy.
backup.successfulJobsHistoryLimitinteger3Number of successful Job records to keep.
backup.failedJobsHistoryLimitinteger3Number of failed Job records to keep.
backup.backoffLimitinteger1Job retry limit.
backup.archivePrefixstringmetabasePrefix for backup archive filenames.
backup.images.postgresqlstringdocker.io/library/postgres:18-alpineImage used for pg_dump.
backup.images.uploaderstringdocker.io/helmforge/mc:1.0.0Image used for S3 upload.
backup.resourcesobject{}Resources for backup containers.
backup.s3.endpointstring""S3-compatible endpoint URL.
backup.s3.bucketstring""Target bucket name.
backup.s3.prefixstringmetabaseKey prefix within the bucket.
backup.s3.createBucketIfNotExistsbooleantrueCreate the bucket automatically if it does not exist.
backup.s3.existingSecretstring""Existing secret containing S3 access and secret keys.
backup.s3.existingSecretAccessKeyKeystringaccess-keyKey in the existing secret for the S3 access key.
backup.s3.existingSecretSecretKeyKeystringsecret-keyKey in the existing secret for the S3 secret key.
backup.s3.accessKeystring""Inline S3 access key (ignored when existingSecret is set).
backup.s3.secretKeystring""Inline S3 secret key (ignored when existingSecret is set).
backup.database.hoststring""Override database host for backup (uses app credentials if empty).
backup.database.portstring""Override database port for backup.
backup.database.namestring""Override database name for backup.
backup.database.usernamestring""Override database username for backup.
backup.database.passwordstring""Override database password for backup.
backup.database.existingSecretstring""Existing secret containing backup database credentials.
backup.database.existingSecretPasswordKeystringpasswordKey in the existing secret for the backup database password.
backup.database.postgresDumpArgsstring""Extra arguments passed to pg_dump.

External Secrets

Set externalSecrets.enabled when External Secrets Operator manages the Metabase application secret. The chart requires metabase.existingSecret so ESO is the only writer for the encryption key.

metabase:
  existingSecret: metabase-app-secret
  existingSecretKey: encryption-secret-key

externalSecrets:
  enabled: true
  secretStoreRef:
    name: cluster-secrets
    kind: ClusterSecretStore
  data:
    - secretKey: encryption-secret-key
      remoteRef:
        key: metabase/credentials
        property: encryption-secret-key
ParameterTypeDefaultDescription
externalSecrets.enabledbooleanfalseRender an ExternalSecret resource.
externalSecrets.apiVersionstringexternal-secrets.io/v1ExternalSecret API version.
externalSecrets.refreshIntervalstring"0"ExternalSecret refresh interval.
externalSecrets.secretStoreRef.namestring""SecretStore or ClusterSecretStore name.
externalSecrets.secretStoreRef.kindstringSecretStoreSecret store kind.
externalSecrets.target.creationPolicystringOwnerTarget Secret creation policy.
externalSecrets.dataarray[]Remote key mappings for the app Secret.

Resources and Security

ParameterTypeDefaultDescription
resourcesobject{}CPU and memory requests and limits.
podSecurityContextobject{}Pod-level security context.
securityContextobject{}Container-level security context.

Service Account

ParameterTypeDefaultDescription
serviceAccount.createbooleanfalseCreate a dedicated ServiceAccount.
serviceAccount.namestring""Override the ServiceAccount name.
serviceAccount.annotationsobject{}Annotations for the ServiceAccount.

Scheduling

ParameterTypeDefaultDescription
nodeSelectorobject{}Node selector for scheduling.
tolerationsarray[]Tolerations for scheduling.
affinityobject{}Affinity rules.
topologySpreadConstraintsarray[]Topology spread constraints.
priorityClassNamestring""PriorityClass for the pod.
terminationGracePeriodSecondsinteger30Termination grace period.
podLabelsobject{}Extra labels for the pod.
podAnnotationsobject{}Extra annotations for the pod.

Extra

ParameterTypeDefaultDescription
extraVolumesarray[]Extra volumes to attach to the pod.
extraVolumeMountsarray[]Extra volume mounts for the container.
extraManifestsarray[]Extra Kubernetes manifests deployed alongside the chart.

Common Issues

Pod stuck in CrashLoopBackOff — OOMKilled

Metabase is a JVM application and requires adequate memory. If the pod is killed without a clear error, check kubectl describe pod <name> for OOMKilled. Set metabase.javaOpts: '-Xms512m -Xmx1g' and configure resources.limits.memory at least 20% above -Xmx.

First startup takes time

Metabase runs database migrations on first boot. With the embedded PostgreSQL subchart, the first startup can take 2–4 minutes. The startup probe is configured with a 5-minute window (failureThreshold: 30 × periodSeconds: 10). Do not reduce these values on a fresh install.

Backing up encryptionSecretKey separately

The S3 backup protects the PostgreSQL data, but encryptionSecretKey is not stored in the database — it is stored in your values or Kubernetes secret. Back it up separately. Without it, a restored database is unusable for re-encrypting credentials.

More Information