If 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 not to 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. If 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 . Galera 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 . TL;DR Ansible this GitHub repository 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. Disclaimer Orange Pi cluster 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. Info popularity Red Hat OpenShift Certified MariaDB Enterprise 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 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. traefik : No need for HTTP ingress. traefik servicelb : I relied on NodePorts instead. servicelb cloud-controller : Irrelevant on bare-metal. cloud-controller network-policy : Avoided for simplicity and memory savings. network-policy 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): <control-node-ip> curl -sfL https://get.k3s.io | \ K3S_URL=https://<control-node-ip>:6443 \ K3S_TOKEN=<token> sh - 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 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. WinSCP WSL <control-node-ip> 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 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: root password documentation kubectl create secret generic mariadb-root-password --from-literal=password=demo123 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: Martin Montes MariaDB plc “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.” “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 . 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. first deployment OOMKilled 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) # 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. Info innodb_buffer_pool_size Warning : Normally, it is recommended not to 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. Warning 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. SST (State Snapshot Transfer) 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) # 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. Running kubectl get pods -o wide Info : To ensure that every MariaDB pod gets scheduled on a different Node, set spec.gallera.affinity.antiAffinityEnabled to true . Info spec.gallera.affinity.antiAffinityEnabled 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;" 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 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. The MariaDB Operator handled resource creation pretty well — PVCs, StatefulSets, Secrets, and lifecycle probes were all applied correctly with no manual intervention. The MariaDB Operator handled resource creation pretty well Galera on SBCs is actually possible . SST needs patience, and tuning memory limits is critical, but it works! Galera on SBCs is actually possible Out-of-the-box kube probes often don’t work on slow hardware . Startup times will trip checks unless you adjust thresholds. Out-of-the-box kube probes often don’t work on slow hardware Node scheduling worked out fine on its own . K3s distributed the pods evenly. Node scheduling worked out fine on its own 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. Failures teach more than success 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. this manifest 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: this repository 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. 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.