Deploy MariaDB on Kubernetes with MariaDB operator

This guide will walk you through deploying MariaDB on a Cloudfleet Kubernetes Engine (CFKE) cluster using the MariaDB operator. MariaDB is fully compatible with MySQL, serving as a drop-in replacement with enhanced features and performance. The MariaDB operator enables you to declaratively manage MariaDB instances using Kubernetes Custom Resource Definitions (CRDs), providing features like high availability, automated backups, and seamless scaling.

What is a Kubernetes operator?

A Kubernetes operator is like having a database expert that automates all the complex operational tasks for your MariaDB deployment. It transforms sophisticated database management into simple, declarative configurations.

While standard Kubernetes resources like Deployments can run your applications, they treat databases like any other container. But databases have unique requirements: careful initialization, coordinated clustering, comprehensive backup strategies, and rolling updates that preserve data integrity.

A Kubernetes operator bridges this gap by encoding operational best practices directly into your cluster. For MariaDB, the operator automatically handles:

  • Database initialization – Configures databases with optimal settings from the start
  • Automated backups – Schedules and manages backups reliably
  • High availability clustering – Orchestrates multi-node setups that survive failures
  • Safe rolling updates – Deploys changes while maintaining data consistency
  • Health monitoring – Continuously monitors database performance and status

The power comes from declarative management. Instead of writing scripts or running manual commands, you simply describe what you want in a YAML file – like “a 3-node MariaDB cluster with daily backups”, and the operator handles all the implementation details automatically.

Prerequisites

Before getting started, ensure you have:

  • A running Cloudfleet Kubernetes Engine (CFKE) cluster (see the getting started guide if you need to set one up)
  • kubectl configured to interact with your cluster (see the Cloudfleet CLI configuration guide if needed)
  • Helm installed on your local machine
  • Storage support configured on your cluster (see storage requirements below)

Storage requirements

MariaDB requires persistent storage to maintain data across pod restarts. Your CFKE cluster must have a storage provisioner configured.

For Hetzner-based clusters

If your cluster uses Hetzner nodes, follow the persistent volumes with Cloudfleet on Hetzner tutorial to set up the Hetzner Cloud CSI driver on your CFKE cluster.

For self-managed nodes

If you’re using self-managed nodes, you can set up the local-path-provisioner:

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml

This creates a local-path storage class that uses local node storage. While suitable for many use cases, be aware that local storage is tied to specific nodes. For production environments requiring high availability across multiple nodes, consider distributed storage solutions like Ceph, Longhorn, or network-attached storage.

Step 1: Install the MariaDB operator

The MariaDB operator consists of two Helm charts: the CRDs and the operator itself.

First, add the MariaDB operator Helm repository:

helm repo add mariadb-operator https://helm.mariadb.com/mariadb-operator
helm repo update

Install the Custom Resource Definitions (CRDs):

helm install mariadb-operator-crds mariadb-operator/mariadb-operator-crds

Install the MariaDB operator:

helm install mariadb-operator mariadb-operator/mariadb-operator

Step 2: Verify the operator installation

Check that the operator pods are running:

kubectl get pods -n default -l app.kubernetes.io/name=mariadb-operator

You should see the MariaDB operator pod in a Running state.

Verify that the MariaDB CRDs have been installed:

kubectl get crd | grep mariadb

This should show several CRDs including mariadbs.k8s.mariadb.com.

Step 3: Create a basic MariaDB instance

Now you can deploy a MariaDB instance using the operator. Create a file named mariadb-basic.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: mariadb-root-password
type: Opaque
stringData:
  password: "your-secure-root-password"
---
apiVersion: v1
kind: Secret
metadata:
  name: mariadb-user-password
type: Opaque
stringData:
  password: "your-secure-app-user-password"
---
apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
  name: mariadb-basic
spec:
  rootPasswordSecretKeyRef:
    name: mariadb-root-password
    key: password

  username: app-user
  passwordSecretKeyRef:
    name: mariadb-user-password
    key: password
  database: myapp

  port: 3306

  storage:
    size: 5Gi
    storageClassName: hcloud-volumes  # Use "local-path" for self-managed nodes

  nodeSelector:
    cfke.io/provider: hetzner  # Required when using hcloud-volumes storage class

  myCnf: |
    [mariadb]
    bind-address=*
    default_storage_engine=InnoDB
    binlog_format=row
    innodb_autoinc_lock_mode=2
    max_allowed_packet=256M    

  resources:
    # Choose appropriate resource requests for the MariaDB pod
    requests:
      cpu: 1000m
      memory: 512Mi

  metrics:
    enabled: true

Important notes about this configuration:

  • Replace "your-secure-root-password" and "your-secure-app-user-password" with strong passwords
  • Use storageClassName: hcloud-volumes for Hetzner clusters or storageClassName: local-path for self-managed nodes
  • Critical for Hetzner storage: The nodeSelector: cfke.io/provider: hetzner ensures MariaDB pods run on Hetzner nodes. Hetzner Cloud volumes can only be attached to Hetzner nodes, so omitting this will cause storage attachment failures. If your cluster only contains Hetzner nodes, this nodeSelector is optional but recommended for clarity
  • Remove the nodeSelector when using local-path storage class for self-managed nodes
  • We’re creating the password secrets explicitly rather than letting the operator generate them, which makes cross-namespace usage simpler
  • Metrics are enabled for monitoring with Prometheus

Apply the configuration:

kubectl apply -f mariadb-basic.yaml

Step 4: Monitor the deployment

Watch the MariaDB instance being created:

kubectl get mariadb mariadb-basic -w

The status will progress from Provisioning to Ready once the database is fully deployed.

Check the created pods:

kubectl get pods -l app.kubernetes.io/instance=mariadb-basic

You should see a MariaDB pod running.

Verify the persistent volume claim:

kubectl get pvc -l app.kubernetes.io/instance=mariadb-basic

Step 5: Connect to your MariaDB instance

Get connection credentials

Retrieve the generated application user password:

kubectl get secret mariadb-user-password -o jsonpath='{.data.password}' | base64 -d

Connect using kubectl port-forward

Forward the MariaDB port to your local machine:

kubectl port-forward svc/mariadb-basic 3306:3306

In another terminal, connect using the MariaDB client:

mysql -h 127.0.0.1 -P 3306 -u app-user -p myapp

Enter the password you retrieved in the previous step.

Create a connection resource

For applications running in the cluster, you can create a Connection resource that provides connection details. The Connection resource creates a standardized way to expose database connection information, including connection strings and credentials, making them easily accessible to applications.

Create mariadb-connection.yaml:

apiVersion: k8s.mariadb.com/v1alpha1
kind: Connection
metadata:
  name: mariadb-basic-connection
spec:
  mariaDbRef:
    name: mariadb-basic
  username: app-user
  passwordSecretKeyRef:
    name: mariadb-user-password
    key: password
  database: myapp

Apply the connection:

kubectl apply -f mariadb-connection.yaml

Applications can now reference this connection to get database credentials and connection details. Let’s test the connection using a MySQL client pod to verify everything works correctly:

apiVersion: batch/v1
kind: Job
metadata:
  name: mariadb-test
spec:
  template:
    spec:
      containers:
      - name: mysql-client
        image: mariadb:11.4
        env:
        - name: MYSQL_PWD
          valueFrom:
            secretKeyRef:
              name: mariadb-user-password
              key: password
        command:
        - bash
        - -c
        - |
          echo "Testing MariaDB connection..."
          mariadb -h mariadb-basic -u app-user myapp \
            --ssl-ca=/etc/ssl/mariadb/ca.crt \
            --ssl-cert=/etc/ssl/mariadb/tls.crt \
            --ssl-key=/etc/ssl/mariadb/tls.key \
            --ssl-verify-server-cert \
            -e "SELECT 'Connection successful!' AS Status, NOW() AS CurrentTime;"          
        volumeMounts:
        - name: tls-certs
          mountPath: /etc/ssl/mariadb
          readOnly: true
      volumes:
      - name: tls-certs
        projected:
          sources:
          - secret:
              name: mariadb-basic-ca-bundle  # CA bundle for server verification
              items:
              - key: ca.crt
                path: ca.crt
          - secret:
              name: mariadb-basic-client-cert  # Client certificate for authentication
              items:
              - key: tls.crt
                path: tls.crt
              - key: tls.key
                path: tls.key
      restartPolicy: Never

Apply the test job:

kubectl apply -f mariadb-test.yaml

Check the test results:

kubectl logs job/mariadb-test

If successful, you should see output like:

Testing MariaDB connection...
+------------------------+---------------------+
| Status                 | CurrentTime         |
+------------------------+---------------------+
| Connection successful! | 2025-09-27 09:30:45 |
+------------------------+---------------------+

The Connection resource automatically creates a secret with a ready-to-use DSN connection string. You can inspect the generated DSN:

kubectl get secret mariadb-basic-connection -o json | jq -r '.data.dsn' | base64 -d

This will show a connection string like:

app-user:o%4f9BdmENb|6p5Z@tcp(mariadb-basic.default.svc.cluster.local:3306)/myapp?timeout=5s&tls=mariadb-mariadb-basic-default-client-mariadb-basic-client-cert

Note that the MariaDB operator enables TLS by default, which is why the DSN includes TLS parameter parameters for secure connections. The Connection resource automatically handles TLS configuration and provides a ready-to-use connection string. Applications need to mount the TLS certificates to establish secure connections - the CA bundle for server verification and the client certificate for authentication.

For production applications, you can use a similar pattern but with a Deployment instead of a Job:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: my-app:latest
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: mariadb-basic-connection  # Secret created by the Connection resource
              key: dsn
        volumeMounts:
        - name: tls-certs
          mountPath: /etc/ssl/mariadb
          readOnly: true
      volumes:
      - name: tls-certs
        projected:
          sources:
          - secret:
              name: mariadb-basic-ca-bundle  # CA bundle for server verification
              items:
              - key: ca.crt
                path: ca.crt
          - secret:
              name: mariadb-basic-client-cert  # Client certificate for authentication
              items:
              - key: tls.crt
                path: tls.crt
              - key: tls.key
                path: tls.key

Step 6: Secure your database with TLS enforcement and NetworkPolicy

TLS enforcement

While MariaDB operator enables TLS by default, it allows both encrypted and unencrypted connections. For enhanced security, you can enforce TLS to reject any unencrypted connection attempts:

apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
  name: mariadb-basic
spec:
  # ... other configuration ...
  tls:
    enabled: true
    required: true  # Enforce TLS - reject unencrypted connections

Apply this change to your existing MariaDB instance:

kubectl patch mariadb mariadb-basic --type='merge' -p='{"spec":{"tls":{"enabled":true,"required":true}}}'

This will trigger a rolling update to enforce TLS connections.

NetworkPolicy for network-level security

CFKE provides NetworkPolicy support to control network traffic between pods. This adds an additional layer of security that’s particularly important in multi-tenancy scenarios where multiple applications share the same cluster. You can use NetworkPolicy to ensure only authorized applications can connect to your MariaDB instance:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mariadb-access-policy
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: mariadb
      app.kubernetes.io/instance: mariadb-basic
  policyTypes:
  - Ingress
  ingress:
  - from:
    # Allow connections from pods with specific labels
    - podSelector:
        matchLabels:
          app: my-app
    # Allow connections from the test job
    - podSelector:
        matchLabels:
          job-name: mariadb-test
    ports:
    - protocol: TCP
      port: 3306

Apply the NetworkPolicy:

kubectl apply -f mariadb-network-policy.yaml

This policy ensures that only pods with the label app: my-app or job-name: mariadb-test can connect to your MariaDB instance on port 3306. All other network traffic to the database will be blocked, providing an additional layer of security.

Why both TLS enforcement AND NetworkPolicy?

  • TLS enforcement secures the data in transit - even if someone intercepts network traffic, they cannot read the encrypted database communications
  • NetworkPolicy controls WHO can establish connections - preventing unauthorized applications from even attempting to connect to your database
  • Multi-tenancy protection - In shared clusters where multiple teams deploy applications, NetworkPolicy prevents Team A’s applications from accidentally or maliciously accessing Team B’s databases, even if they somehow obtain valid credentials

Backup and restore

The MariaDB operator supports automated backups. Create a backup configuration:

apiVersion: k8s.mariadb.com/v1alpha1
kind: Backup
metadata:
  name: mariadb-backup
spec:
  mariaDbRef:
    name: mariadb-basic
  schedule:
    cron: "0 2 * * *"  # Daily at 2 AM
  storage:
    persistentVolumeClaim:
      resources:
        requests:
          storage: 1Gi
      storageClassName: hcloud-volumes
  retentionPolicy:
    cleanupPolicy: Delete
    daysToRetain: 30

Conclusion

You’ve successfully deployed MariaDB on your CFKE cluster using the MariaDB operator. This setup provides:

  • Declarative database management through Kubernetes CRDs
  • Persistent storage for data durability
  • Built-in monitoring capabilities
  • Options for high availability with Galera clustering
  • Automated backup and restore functionality

The MariaDB operator simplifies database operations in Kubernetes while providing enterprise-grade features for production workloads. You can now deploy applications that require MariaDB with confidence, knowing your database infrastructure is managed declaratively and can scale with your needs.

Next steps

Now that you have a basic MariaDB deployment running, you might want to explore more advanced features:

High availability and clustering

Advanced backup strategies

Monitoring and observability

  • Metrics collection - Enable Prometheus metrics for database monitoring (configured via the metrics.enabled: true setting shown in this tutorial)

Production hardening

For detailed information on these advanced features, refer to the MariaDB operator documentation.