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
  • Secure runtime defaults — non-root UID 2000, read-only root filesystem, dropped capabilities, and no API token
  • Optional traffic and disruption controls — NetworkPolicy and PodDisruptionBudget templates

Architecture

The chart runs one Metabase Deployment behind a named-port Service. Application metadata is stored in either the bundled HelmForge PostgreSQL StatefulSet or an external PostgreSQL service; analytical source databases remain outside the release boundary. The application Secret supplies the stable encryption key, and the optional backup CronJob exports the PostgreSQL metadata database to S3-compatible storage.

The application container runs as UID/GID 2000 with a read-only root filesystem. Bounded emptyDir volumes provide the only writable paths at /plugins, /tmp, and /target.

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

After deploying, access Metabase:

kubectl port-forward svc/<release>-metabase 3000:80
# Open http://localhost:3000 to complete the setup wizard

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

Parameter Type Default Description
nameOverride string "" Override the chart name.
fullnameOverride string "" Override the full release name.
commonLabels object {} Extra labels added to all resources.
global object {} Values shared with chart dependencies.

Image

Parameter Type Default Description
image.repository string docker.io/metabase/metabase Metabase container image.
image.tag string "v0.64.2" Image tag.
image.pullPolicy string IfNotPresent Image pull policy.
imagePullSecrets array [] Pull secrets for private registries.

Air-Gapped and Proxied Registries

The chart uses a wait-for-db init container before starting Metabase. Configure waitForDatabase.image.* when your cluster must pull helper images from an internal registry or proxy.

image:
  repository: registry.example.com/proxy/metabase/metabase
  tag: v0.64.2

waitForDatabase:
  image:
    repository: registry.example.com/proxy/library/busybox
    tag: '1.37'
Parameter Type Default Description
waitForDatabase.image.repository string docker.io/library/busybox Wait-for-db init container image repository.
waitForDatabase.image.tag string "1.37" Wait-for-db init container image tag.
waitForDatabase.image.pullPolicy string IfNotPresent Wait-for-db init container image pull policy.

Metabase Configuration

Parameter Type Default Description
metabase.port integer 3000 Internal HTTP port for the Metabase container.
metabase.encryptionSecretKey string "" Secret key used to encrypt saved data source credentials.
metabase.existingSecret string "" Existing secret containing the encryption key.
metabase.existingSecretKey string encryption-secret-key Key inside the existing secret holding the encryption value.
metabase.siteUrl string "" Public URL of the Metabase instance. Used for email links and embeds.
metabase.aiFeaturesEnabled boolean false Enable Metabase AI features after configuring a supported provider.
metabase.javaTimezone string UTC JVM timezone (TZ environment variable).
metabase.javaOpts string "" JVM options for memory tuning (e.g. -Xms512m -Xmx1g).
metabase.extraEnv array [] Extra environment variables injected into the Metabase container.
Always set encryptionSecretKey

The chart generates an encryption key and reuses the existing Kubernetes Secret during connected Helm upgrades. Offline rendering cannot perform that lookup. GitOps and production installations should use metabase.existingSecret or set an explicit stable key, then back it up separately from PostgreSQL.

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

Parameter Type Default Description
postgresql.enabled boolean true Deploy a bundled PostgreSQL subchart for Metabase metadata.
postgresql.architecture string standalone PostgreSQL deployment architecture.
postgresql.auth.database string metabase Database name created by the subchart.
postgresql.auth.username string metabase Database username created by the subchart.
postgresql.auth.password string "" Database password (auto-generated if empty).

Database — External

Parameter Type Default Description
database.external.host string "" External PostgreSQL hostname or IP.
database.external.port string "5432" External PostgreSQL port.
database.external.name string metabase Database name on the external server.
database.external.username string metabase Username for the external database.
database.external.password string "" Password for the external database (plain text — prefer secret).
database.external.existingSecret string "" Existing secret containing the database password.
database.external.existingSecretPasswordKey string password Key inside the existing secret for the password.
database.external.networkPolicyPeers array [] Egress peers for the external metadata database.

Service

Parameter Type Default Description
service.type string ClusterIP Kubernetes service type.
service.port integer 80 Service port exposed to the cluster.
service.annotations object {} Annotations for the Service.
service.ipFamilyPolicy string/null ~ Service IP family policy.
service.ipFamilies array [] Ordered Service IP families.

Dual-Stack Service

service:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

Ingress

Parameter Type Default Description
ingress.enabled boolean false Enable an Ingress resource.
ingress.ingressClassName string traefik Ingress class name.
ingress.annotations object {} Annotations for the Ingress (e.g. cert-manager).
ingress.hosts array [] Ingress host and path rules.
ingress.tls array [] TLS configuration (secret name and hosts).

Gateway API

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

gateway:
  enabled: true
  parentRefs:
    - name: shared-gateway
      namespace: gateway-system
  hostnames:
    - metabase.example.com
Parameter Type Default Description
gateway.enabled boolean false Render a Gateway API HTTPRoute.
gateway.parentRefs array [] Existing Gateway parentRefs, required when enabled.
gateway.hostnames array [] HTTPRoute hostnames.
gateway.path string / HTTPRoute path match value.
gateway.pathType string PathPrefix HTTPRoute path match type.
gateway.annotations object {} Annotations added to the HTTPRoute.

The older gatewayAPI block remains supported as a deprecated compatibility alias.

Probes

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

Parameter Type Default Description
probes.startup.enabled boolean true Enable startup probe (uses /api/health).
probes.startup.initialDelaySeconds integer 90 Startup probe initial delay.
probes.startup.periodSeconds integer 10 Startup probe period.
probes.startup.timeoutSeconds integer 5 Startup probe timeout.
probes.startup.failureThreshold integer 30 Startup probe failure threshold (5 minutes).
probes.liveness.enabled boolean true Enable liveness probe.
probes.liveness.initialDelaySeconds integer 0 Liveness probe initial delay.
probes.liveness.periodSeconds integer 15 Liveness probe period.
probes.liveness.timeoutSeconds integer 5 Liveness probe timeout.
probes.liveness.failureThreshold integer 3 Liveness probe failure threshold.
probes.readiness.enabled boolean true Enable readiness probe.
probes.readiness.initialDelaySeconds integer 0 Readiness probe initial delay.
probes.readiness.periodSeconds integer 10 Readiness probe period.
probes.readiness.timeoutSeconds integer 5 Readiness probe timeout.
probes.readiness.failureThreshold integer 3 Readiness 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.

Parameter Type Default Description
backup.enabled boolean false Enable scheduled S3 backup CronJob.
backup.schedule string "0 3 * * *" Cron schedule for backups.
backup.suspend boolean false Suspend the CronJob without deleting it.
backup.concurrencyPolicy string Forbid CronJob concurrency policy.
backup.successfulJobsHistoryLimit integer 3 Number of successful Job records to keep.
backup.failedJobsHistoryLimit integer 3 Number of failed Job records to keep.
backup.backoffLimit integer 1 Job retry limit.
backup.archivePrefix string metabase Prefix for backup archive filenames.
backup.images.postgresql string docker.io/library/postgres:18-alpine Image used for pg_dump.
backup.images.uploader string docker.io/helmforge/mc:1.0.0 Image used for S3 upload.
backup.resources object {} Resources for backup containers.
backup.podSecurityContext.seccompProfile.type string RuntimeDefault Backup pod seccomp profile.
backup.securityContext.allowPrivilegeEscalation boolean false Block privilege escalation in backup containers.
backup.securityContext.capabilities.drop array [ALL] Drop Linux capabilities in backup containers.
backup.s3.endpoint string "" S3-compatible endpoint URL.
backup.s3.bucket string "" Target bucket name.
backup.s3.prefix string metabase Key prefix within the bucket.
backup.s3.createBucketIfNotExists boolean true Create the bucket automatically if it does not exist.
backup.s3.existingSecret string "" Existing secret containing S3 access and secret keys.
backup.s3.existingSecretAccessKeyKey string access-key Key in the existing secret for the S3 access key.
backup.s3.existingSecretSecretKeyKey string secret-key Key in the existing secret for the S3 secret key.
backup.s3.accessKey string "" Inline S3 access key (ignored when existingSecret is set).
backup.s3.secretKey string "" Inline S3 secret key (ignored when existingSecret is set).
backup.database.host string "" Override database host for backup (uses app credentials if empty).
backup.database.port string "" Override database port for backup.
backup.database.name string "" Override database name for backup.
backup.database.username string "" Override database username for backup.
backup.database.password string "" Override database password for backup.
backup.database.existingSecret string "" Existing secret containing backup database credentials.
backup.database.existingSecretPasswordKey string password Key in the existing secret for the backup database password.
backup.database.postgresDumpArgs string "" 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
Parameter Type Default Description
externalSecrets.enabled boolean false Render an ExternalSecret resource.
externalSecrets.apiVersion string external-secrets.io/v1 ExternalSecret API version.
externalSecrets.refreshInterval string "0" ExternalSecret refresh interval.
externalSecrets.secretStoreRef.name string "" SecretStore or ClusterSecretStore name.
externalSecrets.secretStoreRef.kind string SecretStore Secret store kind.
externalSecrets.target.creationPolicy string Owner Target Secret creation policy.
externalSecrets.data array [] Remote key mappings for the app Secret.

Resources and Security

Parameter Type Default Description
resources.requests.cpu string 250m Application CPU request.
resources.requests.memory string 512Mi Application memory request.
resources.limits.cpu string 1000m Application CPU limit.
resources.limits.memory string 2Gi Application memory limit.
podSecurityContext.runAsNonRoot boolean true Require a non-root runtime identity.
podSecurityContext.runAsUser integer 2000 UID supported by the official image.
podSecurityContext.runAsGroup integer 2000 Primary runtime group.
podSecurityContext.fsGroup integer 2000 Writable volume group ownership.
podSecurityContext.fsGroupChangePolicy string OnRootMismatch Avoid unnecessary ownership changes.
podSecurityContext.seccompProfile.type string RuntimeDefault Apply the runtime’s default seccomp profile.
securityContext.allowPrivilegeEscalation boolean false Block privilege escalation.
securityContext.readOnlyRootFilesystem boolean true Make the root filesystem immutable.
securityContext.capabilities.drop array [ALL] Drop all Linux capabilities.
runtimeStorage.pluginsSizeLimit string 512Mi Limit the writable /plugins volume.
runtimeStorage.tmpSizeLimit string 512Mi Limit the writable /tmp volume.
runtimeStorage.targetSizeLimit string 256Mi Limit the writable /target volume.
serviceAccount.automountServiceAccountToken boolean false Keep the Kubernetes API token out of the pod.

The reserved runtime-plugins volume can be mounted by extraInitContainers to install JDBC drivers without making the application root filesystem writable.

An extraVolumeMounts entry at /plugins, /tmp, or /target replaces the corresponding chart-managed mount. The reserved emptyDir remains declared but unmounted, so its runtimeStorage.*SizeLimit no longer applies to that path. An extraVolumes entry named runtime-plugins, runtime-tmp, or runtime-target replaces the chart-managed volume with that name and remains safe for override mounts that reference the reserved name.

NetworkPolicy and PodDisruptionBudget

networkPolicy.enabled adds ingress isolation. Same-namespace traffic is allowed by default; ingress controllers in other namespaces must be listed in networkPolicy.ingress.extraFrom. Enabling networkPolicy.egress.enabled also requires rules for every analytical database, SMTP server, webhook, and external API. Use database.external.networkPolicyPeers for an external metadata database and networkPolicy.egress.extraEgress for all other destinations.

The optional pdb.enabled template serializes voluntary disruptions. It does not provide high availability for the single default application replica.

Parameter Type Default Description
pdb.enabled boolean false Render a PodDisruptionBudget.
pdb.minAvailable string/int "" Minimum available pods; takes precedence.
pdb.maxUnavailable string/int 1 Maximum unavailable pods when minimum is empty.
pdb.unhealthyPodEvictionPolicy string "" Optional unhealthy pod eviction policy.
networkPolicy.enabled boolean false Render ingress traffic isolation.
networkPolicy.ingress.allowSameNamespace boolean true Allow callers in the release namespace.
networkPolicy.ingress.extraFrom array [] Additional ingress peers.
networkPolicy.egress.enabled boolean false Enforce outbound traffic isolation.
networkPolicy.egress.allowDNS boolean true Permit DNS when egress is enforced.
networkPolicy.egress.extraEgress array [] Complete rules for external destinations.

Service Account

Parameter Type Default Description
serviceAccount.create boolean false Create a dedicated ServiceAccount.
serviceAccount.name string "" Override the ServiceAccount name.
serviceAccount.annotations object {} Annotations for the ServiceAccount.
serviceAccount.automountServiceAccountToken boolean false Mount the Kubernetes API token.

Scheduling

Parameter Type Default Description
nodeSelector object {} Node selector for scheduling.
tolerations array [] Tolerations for scheduling.
affinity object {} Affinity rules.
topologySpreadConstraints array [] Topology spread constraints.
priorityClassName string "" PriorityClass for the pod.
terminationGracePeriodSeconds integer 30 Termination grace period.
podLabels object {} Extra labels for the pod.
podAnnotations object {} Extra annotations for the pod.

Extra

Parameter Type Default Description
extraVolumes array [] Extra volumes to attach to the pod.
extraVolumeMounts array [] Extra volume mounts for the container.
extraInitContainers array [] Additional init containers rendered after wait-for-db.
extraManifests array [] Extra Kubernetes manifests deployed alongside the chart.

Troubleshooting

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.

Upgrade Notes

Metabase v0.64.2 is a patch release with analytics fixes and query optimizations. Back up the application database and preserve the encryption key before upgrading.

Metabase v0.64.1 is the first public maintenance release in the 0.64 line. Back up the Metabase application database, keep the encryption key stable, and review the official Metabase 64 changelog before upgrading. Validate /api/health after rollout; first startup can take several minutes while database migrations run.

The generated application encryption key is reused from the existing Kubernetes Secret during connected Helm upgrades. For GitOps or offline rendering, set metabase.existingSecret to a stable Secret; helm template cannot look up the cluster’s current key. An explicit metabase.encryptionSecretKey takes precedence and must not change after installation.

Metabase 0.64.1 includes the 0.64 feature line and its first maintenance fixes. The existing startup encryption behavior remains relevant: to refuse automatic cleanup of legacy plaintext fields, set MB_DISABLE_LEGACY_STARTUP_ENCRYPTION=true through metabase.extraEnv. The upstream default is false. This option is separate from enabling encryption on an application database that has never been encrypted. The patch also validates LDAP server certificates; verify the trust chain of your LDAP server before upgrading an LDAP-enabled deployment.

Starting with 0.63.16, adding encryption to an existing unencrypted application database requires an explicit migration. Back up the database and key, stop all Metabase instances, and run the upstream enable-encryption JAR command with the same database environment and MB_ENCRYPTION_SECRET_KEY used by the deployment. Restart with that same key. Existing encrypted databases do not need this command. If encryption was already configured and Metabase reports an unencrypted database, investigate and restore a known-good backup; do not automatically re-encrypt it. See Metabase encryption instructions.

For signed database sessions, inject MB_SESSION_SECRET_KEY through metabase.extraEnv using a Kubernetes secretKeyRef. Use a separate stable secret of at least 16 characters. Setting or changing it logs out existing sessions; coordinate activation with users. See the session secret documentation.

More Information