MariaDB

Published: 26 Oct. 2026 Last updated: 26 Oct. 2026

Summary

MariaDB is a popular open-source relational database management system and a community-developed fork of MySQL. It is widely used for web applications, data warehousing, and transactional workloads. MariaDB’s default storage engine, InnoDB, provides ACID-compliant transactions, crash recovery via write-ahead logging (redo log), and row-level locking.

This Application Note explains how to use CloudCasa to back up and restore MariaDB running in Kubernetes. It covers the two most common ways to install MariaDB:

  • Helm chart - MariaDB is installed directly with a Helm chart in replication mode (primary and replica). CloudCasa has been tested with MariaDB 13.1 installed using the Bitnami MariaDB Helm chart.

  • MariaDB operator - MariaDB is created and managed by an operator in a Galera multi-primary cluster. CloudCasa has been tested with MariaDB 11.4 created using the MariaDB Operator.

The backup steps are the same for both methods. The restore steps are different, and are described separately. The steps are expected to work with later versions of MariaDB, and with other installation methods, as well.

See also

Before you start

  • Persistence must be enabled. CloudCasa backs up the data that MariaDB has saved to disk on its Persistent Volumes (PVs). The Bitnami Helm chart and the MariaDB operator both enable persistence by default. If persistence has been explicitly disabled in the Helm chart values or the MariaDB specification, re-enable it before using CloudCasa.

  • InnoDB should be the storage engine. The backup approach described below is designed for InnoDB, which has been the default storage engine since MariaDB 10.2. InnoDB’s write-ahead logging and crash recovery ensure that data on disk is always recoverable from a PV snapshot, even if a small number of transactions were still in memory at the time of the snapshot. See InnoDB Crash Recovery in the MariaDB documentation.

Backup

It is recommended to define a full cluster backup, so that all resources MariaDB needs, including any cluster-scoped resources, are included and can be restored together. If you are defining a more granular backup, ensure that MariaDB namespaces are included, and that the option to backup PVs is enabled (which is the default).

A pre-backup App Hook is used to flush MariaDB data to disk before CloudCasa takes the PV snapshot. This Application Note describes three backup methods, each using a different MariaDB command in the pre-backup hook. All three have been tested with both the Bitnami Helm chart and the MariaDB operator.

Comparison of backup methods

Method

Consistency level

Write blocking

Post-hook needed

Storage engines

FLUSH TABLES WITH READ LOCK (FTWRL)

Application-consistent

Yes — all writes blocked until lock is released

Yes

All (InnoDB, MyISAM, Aria, etc.)

BACKUP STAGE

Crash-consistent

No — DML continues; only DDL is blocked

Yes

InnoDB only

FLUSH TABLES

Crash-consistent

No — no persistent lock

No

InnoDB only

Which method should you choose?

  • If you use only InnoDB tables (the default since MariaDB 10.2), FLUSH TABLES is the simplest option. It flushes dirty pages to disk, does not block writes, and does not require a post-backup hook. InnoDB’s crash recovery ensures a consistent restore.

  • If you need to prevent DDL operations (ALTER TABLE, DROP TABLE, etc.) during the snapshot, use BACKUP STAGE. This is MariaDB’s equivalent of MySQL’s LOCK INSTANCE FOR BACKUP. It blocks DDL while allowing normal DML to continue, and is useful if schema changes might run concurrently with backups.

  • If you use non-InnoDB storage engines (such as MyISAM or Aria) or need a fully application-consistent snapshot with no in-flight transactions, use FLUSH TABLES WITH READ LOCK (FTWRL). This is the most protective option, but it blocks all writes for the duration of the snapshot.

Note

MariaDB does not support the LOCK INSTANCE FOR BACKUP command used by MySQL. Use BACKUP STAGE instead, which provides equivalent functionality. See BACKUP STAGE in the MariaDB documentation.

Step 1: Find your MariaDB pods

Note the namespace where MariaDB is running, then list the pods and their labels:

kubectl get pods -n <mariadb-namespace> --show-labels

Make a note of:

  • A label that can uniquely identify MariaDB server pods. For example:

    • Bitnami Helm chart (replication): app.kubernetes.io/component=primary

    • MariaDB operator (Galera): app.kubernetes.io/name=mariadb

  • The name of the MariaDB container in the pod. For both the Bitnami Helm chart and the MariaDB operator, it is mariadb. To check it:

    kubectl get pod <mariadb-pod> -n <mariadb-namespace> -o jsonpath='{.spec.containers[*].name}'
    

The App Hook runs once on every pod that matches the label. For the Bitnami Helm chart in replication mode, use a label that selects only the primary pod (e.g. app.kubernetes.io/component=primary) — the replicas replicate from the primary and do not need their own flush. For the MariaDB operator (Galera cluster), the hook runs on each member, which is acceptable as each member holds its own copy of the data.

Step 2: Create the pre-backup App Hook

In CloudCasa, go to Configuration → App Hooks and click “Add App Hook”. Fill in the fields as follows:

Name: A name for the hook, for example mariadb-pre-backup.

Type: Pre-backup

Pod selector: The label from Step 1, for example app.kubernetes.io/component = primary or app.kubernetes.io/name = mariadb.

Container name: The container name from Step 1, for example mariadb.

Command: Use one of the following, depending on which backup method you chose:

Method 1: FLUSH TABLES WITH READ LOCK (FTWRL)

sh -c '
  P="${MARIADB_ROOT_PASSWORD:-$(cat "${MARIADB_ROOT_PASSWORD_FILE:-/dev/null}" 2>/dev/null)}"
  export MYSQL_PWD="$P"
  mariadb -u root -e "FLUSH TABLES WITH READ LOCK; DO SLEEP(3600);" &
  LOCK_PID=$!
  echo "$LOCK_PID" > /tmp/cc_ftwrl.pid
  sleep 2
  if ! kill -0 "$LOCK_PID" 2>/dev/null; then
    echo "MariaDB lock session failed - check credentials and server status" >&2
    exit 1
  fi
'

This command:

  • Finds the MariaDB root password from the MARIADB_ROOT_PASSWORD environment variable, or from a file named by MARIADB_ROOT_PASSWORD_FILE. Both the Bitnami Helm chart and the MariaDB operator make the root password available through one of these methods.

  • Runs FLUSH TABLES WITH READ LOCK (FTWRL). This flushes all dirty pages from the InnoDB buffer pool to the tablespace files on disk, flushes the binary log, closes all open tables, and acquires a global read lock. The lock prevents any writes until it is released, ensuring the PV snapshot captures a fully consistent point-in-time state with no in-flight transactions.

  • DO SLEEP(3600) keeps the MariaDB client session open in the background so the lock is held while CloudCasa takes the PV snapshot. The lock is released when the post-backup hook terminates this session (Step 3).

  • The command writes the process ID of the background session to a file, so the post-backup hook can find and stop it.

  • After 2 seconds, it verifies the background session is still running. If it has exited, that means FTWRL failed (wrong password, MariaDB not running, or insufficient privileges), and the hook returns an error.

This method requires a post-backup hook (Step 3).

Method 2: BACKUP STAGE

sh -c '
  P="${MARIADB_ROOT_PASSWORD:-$(cat "${MARIADB_ROOT_PASSWORD_FILE:-/dev/null}" 2>/dev/null)}"
  export MYSQL_PWD="$P"
  mariadb -u root -e "BACKUP STAGE START; BACKUP STAGE BLOCK_DDL; DO SLEEP(3600);" &
  LOCK_PID=$!
  echo "$LOCK_PID" > /tmp/cc_ftwrl.pid
  sleep 2
  if ! kill -0 "$LOCK_PID" 2>/dev/null; then
    echo "Backup stage lock failed - check credentials and server status" >&2
    exit 1
  fi
'

This command:

  • Runs BACKUP STAGE START followed by BACKUP STAGE BLOCK_DDL (BACKUP STAGE). This acquires a backup lock that prevents DDL operations (such as ALTER TABLE and DROP TABLE) while allowing normal DML (inserts, updates, deletes) to continue. This is MariaDB’s equivalent of MySQL’s LOCK INSTANCE FOR BACKUP.

  • Like Method 1, the lock is held in a background session and released by the post-backup hook.

  • This method is suitable only for InnoDB tables. Non-InnoDB tables may not be in a consistent state.

This method requires a post-backup hook (Step 3).

Method 3: FLUSH TABLES

sh -c '
  P="${MARIADB_ROOT_PASSWORD:-$(cat "${MARIADB_ROOT_PASSWORD_FILE:-/dev/null}" 2>/dev/null)}"
  export MYSQL_PWD="$P"
  mariadb -u root -e "FLUSH TABLES;" && echo "Tables flushed"
'

This command:

  • Runs FLUSH TABLES (FLUSH TABLES). This closes all open tables and flushes dirty pages from the InnoDB buffer pool to the tablespace files on disk. It does not acquire any lock — the command completes immediately and writes resume normally.

  • Because there is no lock, a small number of transactions may be in flight at the moment the PV snapshot is taken. InnoDB’s crash recovery automatically replays these from the redo log when MariaDB starts after restore.

  • This is the simplest method and the least intrusive. It is suitable only for InnoDB tables.

This method does not require a post-backup hook. Skip Step 3 if you use this method.

Click Save.

Step 3: Create the post-backup App Hook

Skip this step if you chose Method 3 (FLUSH TABLES). Method 3 does not acquire a lock and does not need a post-backup hook.

Create another App Hook with the following settings:

Name: A name for the hook, for example mariadb-post-backup.

Type: Post-backup

Pod selector: Same label as the pre-backup hook.

Container name: Same container name as the pre-backup hook.

Command:

sh -c '
  if [ -f /tmp/cc_ftwrl.pid ]; then
    kill "$(cat /tmp/cc_ftwrl.pid)" 2>/dev/null || true
    rm -f /tmp/cc_ftwrl.pid
  fi
'

Click Save.

What this command does:

  • It kills the background MariaDB session started by the pre-backup hook. When the session ends, MariaDB automatically releases the global read lock (Method 1) or backup stage lock (Method 2), and normal operations resume.

  • If the PID file does not exist (for example, if the pre-backup hook did not run or failed), the command does nothing and exits successfully.

A post-backup App Hook is required for Methods 1 and 2 because the pre-backup hook leaves a lock in place. Without this hook, the lock would only be released when the background sleep expires (up to 1 hour), during which MariaDB would be read-only (Method 1) or unable to run DDL (Method 2).

Note: A failed pre-backup App Hook does not stop the backup. The MariaDB PVs are still backed up, but CloudCasa marks the backup job as Partially Failed, with a message such as “Backup partially successful. … app hooks failed”. If the pre-backup hook fails, the backed-up data is still recoverable — InnoDB’s crash recovery automatically replays the redo log when MariaDB starts from the restored PVs. Open the job on the Activity page and check its errors and warnings to see why the App Hook failed.

Step 4: Create a backup definition

Create a backup definition following the CloudCasa User Guide. Select a full cluster backup, and in the App Hooks section of the backup definition, add the pre-backup hook created in Step 2. If you chose Method 1 or Method 2, also add the post-backup hook from Step 3.

See also

For more details on defining a backup, see Defining a Kubernetes backup job.

Restore

Restore to a namespace or cluster where MariaDB is not already running. Do not restore over a running MariaDB deployment. When MariaDB starts after the restore, InnoDB automatically runs crash recovery on the restored data files, replaying any pending transactions from the redo log to bring the database to a fully consistent state.

MariaDB installed with a Helm chart

When creating the restore definition, select the namespace where MariaDB is installed. PVs are included by default.

No operator or Custom Resource Definitions (CRDs) are needed. The restored StatefulSets start the MariaDB pods directly.

If MariaDB was installed in replication mode (primary and replicas), all instances are restored with their own PVs. Replication resumes automatically after the pods start — the replica reconnects to the primary using the binary log position saved before the backup. See MariaDB Replication Overview in the MariaDB documentation.

Important: Always restore the full namespace, including all PVs. Do not attempt to restore only the primary PV while excluding the replica PV. In testing, selective restores where the replica PV was excluded did not work reliably — the replica pod either failed to start (because its StatefulSet was also excluded from the restore), or started with a fresh empty database instead of cloning from the primary. This resulted in data mismatches between primary and replica, and new writes on the primary caused replication errors (Duplicate entry for key 'PRIMARY'). Restoring the full namespace avoids these issues. See Replication Errors in the MariaDB documentation for more information on troubleshooting replication issues.

MariaDB installed with the MariaDB operator (Galera)

When creating the restore definition, select the namespace where MariaDB is installed.

Also select the namespace where the MariaDB operator is installed, if the operator is not already running in the target cluster. The operator manages the MariaDB resources after they are restored. If the operator is not running, the restored MariaDB resources may not start correctly. If the operator is already running in the target cluster, restoring its namespace again is optional.

Important: Make sure the “Include all cluster-scoped resources” switch is enabled when creating the restore definition. This restores the CRDs for MariaDB resources, such as MariaDB, Grant, and User.

Important: Always restore all Galera node PVs together. Do not attempt to restore only a subset of nodes. All nodes in the Galera cluster must be restored from the same backup to ensure data consistency. Galera does not automatically reconcile diverged data between nodes — it only replicates new writes after the cluster re-forms. See Galera Cluster Recovery in the MariaDB documentation.

After the restore completes, the Galera cluster may need manual intervention to bootstrap. See Galera cluster stuck in bootstrap loop after restore in the Troubleshooting section below.

Check the restored data

After the restore completes, check that the MariaDB pods are running:

kubectl get pods -n <mariadb-namespace>

Then check that your databases and data are intact:

kubectl exec -n <mariadb-namespace> <mariadb-pod> -c mariadb -- sh -c 'export MYSQL_PWD="${MARIADB_ROOT_PASSWORD:-$(cat ${MARIADB_ROOT_PASSWORD_FILE:-/dev/null} 2>/dev/null)}"; mariadb -u root -e "SHOW DATABASES;"'

You can also verify the row count for a specific table:

kubectl exec -n <mariadb-namespace> <mariadb-pod> -c mariadb -- sh -c 'export MYSQL_PWD="${MARIADB_ROOT_PASSWORD:-$(cat ${MARIADB_ROOT_PASSWORD_FILE:-/dev/null} 2>/dev/null)}"; mariadb -u root -e "SELECT COUNT(*) FROM <your_database>.<your_table>;"'

The list of databases and row counts should match what was present before the backup.

See also

For more information on defining a restore see Cluster Restore Wizard.

Troubleshooting

Galera cluster stuck in bootstrap loop after restore

After restoring a MariaDB Galera cluster from a PV snapshot backup, the Galera cluster may fail to start with the MariaDB resource showing:

Error reconciling Galera: error recovering cluster: error getting bootstrap source: bootstrap source not found

This happens because PV snapshots taken while Galera is running contain uuid: 00000000-0000-0000-0000-000000000000 in the grastate.dat file on each node. The MariaDB operator’s recovery logic cannot determine which node should bootstrap the cluster when all nodes have a zero UUID. See Getting Started with MariaDB Galera Cluster — Bootstrapping and Galera Cluster System Variables — wsrep_cluster_state_uuid in the MariaDB documentation for background on Galera cluster bootstrapping and state UUIDs.

To fix this, patch the MariaDB resource to force the cluster to bootstrap from a specific pod:

kubectl patch mariadb <mariadb-name> -n <mariadb-namespace> --type merge \
  -p '{"spec":{"galera":{"recovery":{"forceClusterBootstrapInPod":"<mariadb-pod-0>"}}}}'

For example:

kubectl patch mariadb mariadb-galera -n mariadb-galera --type merge \
  -p '{"spec":{"galera":{"recovery":{"forceClusterBootstrapInPod":"mariadb-galera-0"}}}}'

After patching, the operator bootstraps the cluster from the specified pod. The other nodes join automatically via SST (State Snapshot Transfer), receiving a full copy of the data from the bootstrap node. The cluster forms with all nodes in a Synced state and normal replication resumes.

Note: This issue does not always occur. In some restores, the operator’s built-in recovery logic is able to bootstrap the cluster automatically. When it does not, the patch command above resolves the issue.

MariaDB operator webhook blocks namespace deletion

When deleting a MariaDB Galera namespace after the operator namespace has already been removed, namespace deletion may hang with finalizers remaining on MariaDB Grant and User resources:

Some content in the namespace has finalizers remaining: grant.k8s.mariadb.com/finalizer in 1 resource instances,
user.k8s.mariadb.com/finalizer in 1 resource instances

This happens because the operator’s validating webhook is still registered but the webhook service no longer exists.

To fix this:

  1. Delete the validating webhook configuration:

    kubectl delete validatingwebhookconfiguration mariadb-operator-webhook
    
  2. Remove the finalizers from the stuck resources:

    kubectl patch grant <grant-name> -n <mariadb-namespace> --type merge -p '{"metadata":{"finalizers":null}}'
    kubectl patch user <user-name> -n <mariadb-namespace> --type merge -p '{"metadata":{"finalizers":null}}'
    

The namespace will then terminate normally.

Note: To avoid this issue, delete the MariaDB resource first and wait until it is fully removed before deleting the operator namespace. If both are deleted at the same time, the cleanup can get stuck because the operator is needed to process the finalizers.