Skip to main content

Metabase Data Visualization - Kubernetes Deployment with Kubernetes Operator

638 words·
Metabase Kubernetes Kubernetes-Operator Helm Data Visualization Data Warehouse
Table of Contents

My Setup
#

In this tutorial I’m using the following kubeadm based Kubernetes cluster, with Nginx Ingress Controller and NFS CSI:

NAME      STATUS   ROLES           AGE    VERSION    INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
ubuntu1   Ready    control-plane   167d   v1.29.11   192.168.30.10   <none>        Ubuntu 24.04.1 LTS   6.8.0-49-generic   containerd://1.7.23
ubuntu2   Ready    worker          167d   v1.29.11   192.168.30.11   <none>        Ubuntu 24.04.1 LTS   6.8.0-49-generic   containerd://1.7.23
ubuntu3   Ready    worker          167d   v1.29.11   192.168.30.12   <none>        Ubuntu 24.04.1 LTS   6.8.0-49-generic   containerd://1.7.23
ubuntu4   Ready    worker          167d   v1.29.11   192.168.30.13   <none>        Ubuntu 24.04.1 LTS   6.8.0-49-generic   containerd://1.7.23

My NFS CSI storage class looks like this:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io # NFS CSI Driver
parameters:
  server: 192.168.30.15
  share: /srv/nfs/k8s_nfs-csi
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
  - nfsvers=3



Metabase Setup
#

Create Namespace
#

# Create "metabase" namespace
kubectl create ns metabase

Helm
#

Install Helm
#

Make sure Helm is installed:

# Install Helm with script
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &&
chmod +x get_helm.sh &&
./get_helm.sh
# Verify the installation / check version
helm version

Add Helm Repository
#

# Add the Metabase-Operator Helm repository
helm repo add metabase-operator-charts https://unagex.github.io/metabase-operator

# Update all Helm repositories
helm repo update
# Optional: List available charts
helm search repo metabase-operator-charts -l

Install Metabase Operator
#

# Install the Metabase Operator
helm install metabase-operator metabase-operator-charts/metabase-operator \
  --namespace metabase

# Shell output:
NAME: metabase-operator
LAST DEPLOYED: Sat May 10 09:19:17 2025
NAMESPACE: metabase
STATUS: deployed
REVISION: 1
TEST SUITE: None



Deploy Metabase Instance
#

Example configuration: https://raw.githubusercontent.com/unagex/metabase-operator/main/config/samples/v1_metabase.yaml

  • metabase-playground.yml
apiVersion: unagex.com/v1
kind: Metabase
metadata:
  name: metabase-playground
  namespace: metabase
spec:
  metabase:
    image: "metabase/metabase:latest"
    imagePullPolicy: "IfNotPresent"
    resources:
      requests:
        cpu: 1
        memory: 2Gi
  db:
    image: "postgres:latest"
    imagePullPolicy: "IfNotPresent"
    replicas: 1
    resources:
      requests:
        cpu: 1
        memory: 2Gi
    volume:
      storageClassName: nfs-csi  # Define storage class
      size: 10Gi
# Deploy Metabase instance
kubectl apply -f metabase-playground.yml

# Delete Metabase instance
kubectl delete -f metabase-playground.yml

Verify Installation
#

Verify Metabase Instance
#

# List Metabase instance
kubectl get metabase -n metabase

# Shell output:
NAME                  AGE
metabase-playground   73s
# List Metabase instance details
kubectl describe metabase metabase-playground -n metabase

Verify Pods
#

# Verify Metabase pods
kubectl get pods -n metabase

# Shell output:
NAME                                   READY   STATUS    RESTARTS   AGE
metabase-operator-56d5dc4b8b-cdkzk     1/1     Running   0          3m55s
metabase-playground-0                  1/1     Running   0          2m17s
metabase-playground-74b58fc9cc-kdvj8   1/1     Running   0          2m17s

Verify Service
#

# Verify the service
kubectl get svc -n metabase

# Shell output:
NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
metabase-playground-http   ClusterIP   10.105.215.200   <none>        3000/TCP   2m46s
metabase-playground-psql   ClusterIP   10.102.116.92    <none>        5432/TCP   2m46s

Verify PVC & PV
#

# Verify PVC
kubectl get pvc -n metabase

# Shell output:
NAME                                                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
metabase-playground-storage-metabase-playground-0   Bound    pvc-f9afb5a9-00ef-48ee-b3af-6630d27c07c1   10Gi       RWO            nfs-csi        <unset>                 3m3s
# Verify PV
kubectl get pv

# Shell output:
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                        STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-f9afb5a9-00ef-48ee-b3af-6630d27c07c1   10Gi       RWO            Delete           Bound    metabase/metabase-playground-storage-metabase-playground-0   nfs-csi        <unset>                          5m13s



Ingress Resource
#

Kubernetes Secret
#

I’m using an Let’s Encrypt wildcard certificate in this tutorial, since my K8s cluster is running locally.

# Create a Kubernetes secret for the TLS certificate
kubectl create secret tls metabase-tls \
  --cert=./fullchain.pem \
  --key=./privkey.pem \
  -n metabase

Create Ingress Resource
#

# Create Ingress configuration
vi metabase-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: metabase-ingress
  namespace: metabase
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - metabase.jklug.work
      secretName: metabase-tls
  rules:
    - host: metabase.jklug.work
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: metabase-playground-http
                port:
                  number: 3000
# Deploy Ingress resource
kubectl apply -f metabase-ingress.yaml

# Delete Ingress resource
kubectl delete -f metabase-ingress.yaml

Verify Ingress Resource
#

# List Ingress resources
kubectl get ingress -n metabase

# Shell output: (Wait a view minutes)
NAME               CLASS   HOSTS                 ADDRESS          PORTS     AGE
metabase-ingress   nginx   metabase.jklug.work   192.168.30.200   80, 443   11s

DNS Entry
#

# Add a DNS entry
192.168.30.200	metabase.jklug.work



Metabase Webinterface
#

Metabase GUI: https://metabase.jklug.work


Metabase includes an example database that can be used to check out it’s data visualization capabilities: