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_dumpof 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
ipFamilyPolicyandipFamilies - 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.comConfiguration 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. |
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.
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
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.
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.
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.