Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
Alejandro Duarte tests a MariaDB Galera cluster on a budget Orange Pi Kubernetes setup, tuning settings for resource limits and confirming successful replication.
Join the DZone community and get the full member experience.
Join For FreeIf you're like me, learning how to run databases inside Kubernetes sounds better when it's hands-on, physical, and brutally honest. So instead of spinning up cloud VMs or using Kind or minikube on a laptop, I went small and real: four Orange Pi 3 LTS boards (a Raspberry Pi alternative), each with just 2GB RAM.
My goal? Get MariaDB — and eventually Galera replication — running on Kubernetes using the official MariaDB Kubernetes Operator.
TL;DR: If you came here for the code, you can find Ansible playbooks on this GitHub repository, along with instructions on how to use them. For production environments, see this manifest.
Disclaimer: This isn’t a tutorial on building an Orange Pi cluster, or even setting up K3s. It’s a record of what I tried, what worked, what broke, and what I learned when deploying MariaDB on Kubernetes.
This article ignores best practices and security in favor of simplicity and brevity of code. The setup presented here helps you to get started with the MariaDB Kubernetes Operator so you can continue your exploration with the links provided at the end of the article.
Info: The MariaDB Kubernetes Operator has been in development since 2022 and is steadily growing in popularity. It’s also Red Hat OpenShift Certified and available as part of MariaDB Enterprise. Galera is a synchronous multi-primary cluster solution that enables high availability and data consistency across MariaDB nodes.
Stripping K3s Down to the Essentials
First of all, I installed K3s (a certified Kubernetes distribution built for IoT and edge computing) on the control node as follows (ssh into the control node):
curl -sfL https://get.k3s.io | \
INSTALL_K3S_EXEC="--disable traefik \
--disable servicelb \
--disable cloud-controller \
--disable network-policy" \
sh -s - server --cluster-init
These flags strip out components I didn't need:
- traefik: No need for HTTP ingress.
- servicelb: I relied on NodePorts instead.
- cloud-controller: Irrelevant on bare-metal.
- network-policy: Avoided for simplicity and memory savings.
On worker nodes, I installed K3s and joined the cluster with the usual command (replace <control-node-ip>
with the actual IP of the control node):
curl -sfL https://get.k3s.io | \
K3S_URL=https://<control-node-ip>:6443 \
K3S_TOKEN=<token> sh -
To be able to manage the cluster from my laptop (MacOS), I did this:
scp orangepi@<master-ip>:/etc/rancher/k3s/k3s.yaml ~/.kube/config
sed -i -e 's/127.0.0.1/<control-node-ip>/g' ~/.kube/config
Windows users can do the same using WinSCP or WSL + scp. And don’t forget to replace <control-node-ip>
with the actual IP again.
Installing the MariaDB Operator
Here’s how I installed the MariaDB Kubernetes operator via Helm (ssh into the control node):
helm repo add mariadb-operator https://helm.mariadb.com/mariadb-operator
helm install mariadb-operator-crds mariadb-operator/mariadb-operator-crds
helm install mariadb-operator mariadb-operator/mariadb-operator
It deployed cleanly with no extra config, and the ARM64 support worked out of the box. Once installed, the operator started watching for MariaDB resources.
The MariaDB Secret
I tried to configure the MariaDB root password in the same manifest file (for demo purposes), but it failed, especially with Galera. I guess the MariaDB servers are initialized before the secret, which makes the startup process fail. So, I just followed the documentation (as one should always do!) and created the secret via command line:
kubectl create secret generic mariadb-root-password --from-literal=password=demo123
I also got the opportunity to speak with Martin Montes (Sr. Software Engineer at MariaDB plc and main developer of the MariaDB Kubernetes Operator). He shared this with me:
“If the rootPasswordSecretKeyRef field is not set, a random one is provisioned by the operator. Then, the init jobs are triggered with that secret, which ties the database's initial state to that random secret. To start over with an explicit secret, you can delete the MariaDB resource, delete the PVCs (which contain the internal state), and create a manifest that contains both the MariaDB and the Secret. It should work.”
You can find some examples of predictable password handling here.
Minimal MariaDB Instance: The Tuning Game
My first deployment failed immediately: OOMKilled
. The MariaDB Kubernetes Operator is made for real production environments, and it works out of the box on clusters with enough compute capacity.
However, in my case, with only 2GB per node, memory tuning was unavoidable. Fortunately, one of the strengths of the MariaDB Kubernetes Operator is its flexible configuration. So, I limited memory usage, dropped buffer pool size, reduced connection limits, and tweaked probe configs to prevent premature restarts.
Here’s the config that ran reliably:
# MariaDB instance
apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
name: mariadb-demo
spec:
rootPasswordSecretKeyRef: # Reference to a secret containing root password for security
name: mariadb-root-password
key: password
storage:
size: 100Mi # Small storage size to conserve resources on limited-capacity SD cards
storageClassName: local-path # Local storage class for simplicity and performance
resources:
requests:
memory: 512Mi # Minimum memory allocation - suitable for IoT/edge devices like Raspberry Pi, Orange Pi, and others
limits:
memory: 512Mi # Hard limit prevents MariaDB from consuming too much memory on constrained devices
myCnf: |
[mariadb]
# Listen on all interfaces to allow external connections
bind-address=0.0.0.0
# Disable binary logging to reduce disk I/O and storage requirements
skip-log-bin
# Set to ~70% of available RAM to balance performance and memory usage
innodb_buffer_pool_size=358M
# Limit connections to avoid memory exhaustion on constrained hardware
max_connections=20
startupProbe:
failureThreshold: 40 # 40 * 15s = 10 minutes grace
periodSeconds: 15 # check every 15 seconds
timeoutSeconds: 10 # each check can take up to 10s
livenessProbe:
failureThreshold: 10 # 10 * 60s = 10 minutes of failing allowed
periodSeconds: 60 # check every 60 seconds
timeoutSeconds: 10 # each check can take 10s
readinessProbe:
failureThreshold: 10 # 10 * 30s = 5 minutes tolerance
periodSeconds: 30 # check every 30 seconds
timeoutSeconds: 5 # fast readiness check
---
# NodePort service
apiVersion: v1
kind: Service
metadata:
name: mariadb-demo-external
spec:
type: NodePort # Makes the database accessible from outside the cluster
selector:
app.kubernetes.io/name: mariadb # Targets the MariaDB pods created by operator
ports:
- protocol: TCP
port: 3306 # Standard MariaDB port
targetPort: 3306 # Port inside the container
nodePort: 30001 # External access port on all nodes (limited to 30000-32767 range)
The operator generated the underlying StatefulSet and other resources automatically. I checked logs and resources — it created valid objects, respected the custom config, and successfully managed lifecycle events. That level of automation saved time and reduced YAML noise.
Info: Set the innodb_buffer_pool_size
variable to around 70% of the total memory.
Warning: Normally, it is recommended to not set CPU limits. This can make the whole initialization process and the database itself slow (and cause CPU throttling). The trade-off of not setting limits is that it might steal CPU cycles from other workloads running on the same Node.
Galera Cluster: A Bit of Patience Required
Deploying a 3-node MariaDB Galera cluster wasn’t that difficult after the experience gained from the single-instance deployment — it only required additional configuration and minimal adjustments. The process takes some time to complete, though. So be patient if you are trying this on small SBCs with limited resources like the Orange Pi or Raspberry Pi.
SST (State Snapshot Transfer) processes are a bit resource-heavy, and early on, the startup probe would trigger restarts before nodes could sync on these small SBCs already running Kubernetes. I increased probe thresholds and stopped trying to watch the rollout step-by-step, instead letting the cluster come up at its own pace.
And it just works! By the way, this step-by-step rollout is designed to avoid downtime: rolling the replicas one at a time, waiting for each of them to sync, proceeding with the primary, and switching over to an up-to-date replica. Also, for this setup, I increased the memory a bit to let Galera do its thing.
Here’s the deployment manifest file that worked smoothly:
# 3-node multi-master MariaDB cluster
apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
name: mariadb-galera
spec:
replicas: 3 # Minimum number for a fault-tolerant Galera cluster (balanced for resource constraints)
replicasAllowEvenNumber: true # Allows cluster to continue if a node fails, even with even number of nodes
rootPasswordSecretKeyRef:
name: mariadb-root-password # References the password secret created with kubectl
key: password
generate: false # Use existing secret instead of generating one
storage:
size: 100Mi # Small storage size to accommodate limited SD card capacity on Raspberry Pi, Orange Pi, and others
storageClassName: local-path
resources:
requests:
memory: 1Gi # Higher than single instance to accommodate Galera overhead
limits:
memory: 1Gi # Strict limit prevents OOM issues on resource-constrained nodes
galera:
enabled: true # Activates multi-master synchronous replication
sst: mariabackup # State transfer method that's more efficient for limited bandwidth connections
primary:
podIndex: 0 # First pod bootstraps the cluster
providerOptions:
gcache.size: '64M' # Reduced write-set cache for memory-constrained environment
gcache.page_size: '64M' # Matching page size improves memory efficiency
myCnf: |
[mariadb]
# Listen on all interfaces for cluster communication
bind-address=0.0.0.0
# Required for Galera replication to work correctly
binlog_format=ROW
# ~70% of available memory for database caching
innodb_buffer_pool_size=700M
# Severely limited to prevent memory exhaustion across replicas
max_connections=12
affinity:
antiAffinityEnabled: true # Ensures pods run on different nodes for true high availability
startupProbe:
failureThreshold: 40# 40 * 15s = 10 minutes grace
periodSeconds: 15 # check every 15 seconds
timeoutSeconds: 10 # each check can take up to 10s
livenessProbe:
failureThreshold: 10 # 10 * 60s = 10 minutes of failing allowed
periodSeconds: 60 # check every 60 seconds
timeoutSeconds: 10 # each check can take 10s
readinessProbe:
failureThreshold: 10 # 10 * 30s = 5 minutes tolerance
periodSeconds: 30 # check every 30 seconds
timeoutSeconds: 5 # fast readiness check
---
# External access service
apiVersion: v1
kind: Service
metadata:
name: mariadb-galera-external
spec:
type: NodePort # Makes the database accessible from outside the cluster
selector:
app.kubernetes.io/name: mariadb # Targets all MariaDB pods for load balancing
ports:
- protocol: TCP
port: 3306 # Standard MariaDB port
targetPort: 3306 # Port inside the container
nodePort: 30001 # External access port on all cluster nodes (using any node IP)
After tuning the values, all three pods reached Running
. I confirmed replication was active, and each pod landed on a different node — kubectl get pods -o wide
confirmed even distribution.
Info: To ensure that every MariaDB pod gets scheduled on a different Node, set spec.gallera.affinity.antiAffinityEnabled
to true
.
Did Replication Work?
Here’s the basic test I used to check if replication worked:
kubectl exec -it mariadb-galera-0 -- mariadb -uroot -pdemo123 -e "
CREATE DATABASE test;
CREATE TABLE test.t (id INT PRIMARY KEY AUTO_INCREMENT, msg TEXT);
INSERT INTO test.t(msg) VALUES ('It works!');"
kubectl exec -it mariadb-galera-1 -- mariadb -uroot -pdemo123 -e "SELECT * FROM test.t;"
kubectl exec -it mariadb-galera-2 -- mariadb -uroot -pdemo123 -e "SELECT * FROM test.t;"
The inserted row appeared on all three nodes. I didn’t measure write latency or SST transfer duration—this wasn’t a performance test. For me, it was just enough to confirm functional replication and declare success.
Since I exposed the service using a simple NodePort, I was also able to connect to the MariaDB cluster using the following:
mariadb -h <master-ip> --port 30001 -u root -pdemo123
I skipped Ingress entirely to keep memory usage and YAML code minimal.
What I Learned
- The MariaDB Operator handled resource creation pretty well — PVCs, StatefulSets, Secrets, and lifecycle probes were all applied correctly with no manual intervention.
- Galera on SBCs is actually possible. SST needs patience, and tuning memory limits is critical, but it works!
- Out-of-the-box kube probes often don’t work on slow hardware. Startup times will trip checks unless you adjust thresholds.
- Node scheduling worked out fine on its own. K3s distributed the pods evenly.
- Failures teach more than success. Early OOM errors helped me understand the behavior of stateful apps in Kubernetes much more than a smooth rollout would’ve.
Final Thoughts
This wasn’t about benchmarks, and it wasn’t for production. For production environments, see this manifest. This article was about shrinking a MariaDB Kubernetes deployment to get it working on a constrained environment. It was also about getting started with the MariaDB Kubernetes Operator and learning what it does for you.
The operator simplified a lot of what would otherwise be painful on K8s: it created stable StatefulSets, managed volumes and config, and coordinated cluster state without needing glue scripts or sidecars. Still, it required experimentation on this resource-limited cluster. Probes need care. And obviously, you won’t get resilience or high throughput from an SBC cluster like this, especially if you have a curious dog or cat around your cluster! But this is a worthwhile test for learning and experimentation. Also, if you don’t want to fiddle with SBCs, try Kind or minikube.
By the way, the MariaDB Kubernetes Operator can do much more for you. Check this repository to see a list of the possibilities. Here are just a few worth exploring next:
- Multiple HA modes: Galera Cluster or MariaDB Replication.
- Advanced HA with MaxScale: a sophisticated database proxy, router, and load balancer for MariaDB.
- Flexible storage configuration. Volume expansion.
- Take, restore and schedule backups.
- Cluster-aware rolling update: roll out replica Pods one by one, wait for each of them to become ready, and then proceed with the primary Pod, using ReplicasFirstPrimaryLast.
- Issue, configure and rotate TLS certificates and CAs.
- Orchestrate and schedule sql scripts.
- Prometheus metrics via mysqld-exporter and maxscale-exporter.
Opinions expressed by DZone contributors are their own.
Comments