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:




