MariaDB – Réplication asynchrone (Master → Slave) sous Docker

Docker : MariaDB – HA par réplication

Besoin de haute disponibilité et continuité de service : j’ai conçu, déployé et validé une réplication asynchrone MariaDB (Master→Slave) avec binlog ROW, utilisateur dédié, persistance et réseau isolé — le tout Dockerisé pour être simple, reproductible et portable.

Docker Compose MariaDB 11 Replication Binlog ROW Debian 12 HA / DR
  • Périmètre : 2 services (master & slave) + validation fonctionnelle
  • Livrables : compose, .env.example, confs my.cnf, HOWTO
  • Sécurité : user de réplication dédié, read_only sur le slave
Voir le repository GitHub
1) Contexte & objectif

Contexte : besoin de HA/continuité (panne du principal, maintenance, lectures intensives).

Objectif pédagogique : démontrer la conception, le déploiement et la validation d’une réplication asynchrone MariaDB.

Contraintes : environnement simple, reproductible, portable → Docker + Docker Compose sur Debian 12.

Périmètre : 2 conteneurs (mariadb_master, mariadb_slave), binlog ROW, validation par création/lecture d’une table test, doc pas-à-pas & bonnes pratiques.

2) Architecture logique
[Docker network: dbnet]
             ┌───────────────────────┐
             │  mariadb_master       │
             │  - server-id: 1       │
   writes →  │  - log_bin ON (ROW)   │
 binlog ---> │  - user 'repl'        │
             └─────────┬─────────────┘
                       │  CHANGE MASTER TO…
                       ▼
             ┌───────────────────────┐
             │  mariadb_slave        │
             │  - server-id: 2       │
             │  - read_only ON       │
             │  - Slave_IO/SQL YES   │
             └───────────────────────┘
  • Réseau : bridge Docker dédié dbnet (isolation, DNS interne).
  • Volumes : data persistants côté master & slave.
3) Arborescence du projet & rôle des fichiers
mariadb-ha-replication/
├─ docker-compose.yml              # Orchestration des 2 services
├─ .env.example                    # Variables (mdp root, DB démo, user réplication…)
├─ .gitignore                      # Ignore volumes/data/dumps/secrets
├─ master/
│  └─ my.cnf                       # conf master (binlog, ROW, server-id)
├─ slave/
│  └─ my.cnf                       # conf slave (read_only, server-id)
└─ docs/
   └─ HOWTO.md                     # Procédure détaillée & troubleshooting

Pourquoi Compose ? Démarrer/arrêter la stack en un bloc, déclarer volumes, réseau, healthchecks & dépendances.

3.1 docker-compose.yml (ce que j’ai écrit et pourquoi)
version: "3.9"

networks:
  dbnet:

volumes:
  master-data:
  slave-data:

services:
  mariadb_master:
    image: mariadb:11
    container_name: mariadb_master
    restart: unless-stopped
    environment:
      - MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MARIADB_DATABASE=${DEMO_DB}
    ports:
      - "3307:3306"              # (optionnel) accès depuis l'hôte
    networks: [dbnet]
    volumes:
      - master-data:/var/lib/mysql
      - ./master/my.cnf:/etc/mysql/conf.d/master.cnf:ro
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -uroot -p${MYSQL_ROOT_PASSWORD} --silent"]
      interval: 5s
      timeout: 3s
      retries: 20

  mariadb_slave:
    image: mariadb:11
    container_name: mariadb_slave
    restart: unless-stopped
    environment:
      - MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    networks: [dbnet]
    depends_on:
      mariadb_master:
        condition: service_healthy
    volumes:
      - slave-data:/var/lib/mysql
      - ./slave/my.cnf:/etc/mysql/conf.d/slave.cnf:ro
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -uroot -p${MYSQL_ROOT_PASSWORD} --silent"]
      interval: 5s
      timeout: 3s
      retries: 20

Points clés : image mariadb:11, volumes persistants, healthchecks, réseau isolé, confs injectées en read-only, depends_on pour séquencer.

3.2 master/my.cnf & 3.3 slave/my.cnf

master/my.cnf :

[mysqld]
server-id=1
log_bin=mysql-bin
binlog_format=ROW

Pourquoi ROW ? Journalise les lignes modifiées (pas seulement la requête) → réplication plus fiable.

slave/my.cnf :

[mysqld]
server-id=2
read_only=ON
relay_log=relay-bin

read_only=ON limite les écritures accidentelles côté slave (hors root local).

3.4 .env.example & 3.5 .gitignore
MYSQL_ROOT_PASSWORD=ChangeMeStrong!
DEMO_DB=demo
REPL_USER=repl
REPL_PASSWORD=ChangeMeRepl!

Pourquoi .env ? Centraliser secrets/variables sans les versionner. Fournir .env.example pour guider.

.gitignore : ignorer **/data/**, *.sql, .env pour ne pas exposer de données/secrets.

4) Déploiement pas à pas (ce que j’ai fait)
# 4.1 Lancer l'infra
cp .env.example .env
docker compose up -d
docker ps

# 4.2 Préparer la réplication côté master (user dédié)
docker exec -it mariadb_master mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "
  CREATE USER '${REPL_USER}'@'%' IDENTIFIED BY '${REPL_PASSWORD}';
  GRANT REPLICATION SLAVE ON *.* TO '${REPL_USER}'@'%';
  FLUSH PRIVILEGES;"

# Verrouiller brièvement & relever FILE/POS
docker exec -it mariadb_master mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "
  FLUSH TABLES WITH READ LOCK;
  SHOW MASTER STATUS;"

# (Optionnel) déverrouiller
docker exec -it mariadb_master mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "UNLOCK TABLES;"

# 4.3 Attacher le slave (adapter FILE/POS relevés)
docker exec -it mariadb_slave mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "
  CHANGE MASTER TO
    MASTER_HOST='mariadb_master',
    MASTER_USER='${REPL_USER}',
    MASTER_PASSWORD='${REPL_PASSWORD}',
    MASTER_LOG_FILE='mysql-bin.000002',
    MASTER_LOG_POS=631;
  START SLAVE;"

# 4.4 Vérifier l'état de réplication
docker exec -it mariadb_slave mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW SLAVE STATUS\\G"
# → Slave_IO_Running: Yes / Slave_SQL_Running: Yes

# 4.5 Test fonctionnel (preuve)
docker exec -it mariadb_master mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "
  USE ${DEMO_DB};
  CREATE TABLE IF NOT EXISTS test_ecf (id INT AUTO_INCREMENT PRIMARY KEY, stamp VARCHAR(64));
  INSERT INTO test_ecf(stamp) VALUES('2025');
  SELECT * FROM test_ecf;"

docker exec -it mariadb_slave mariadb -uroot -p"$MYSQL_ROOT_PASSWORD" -e "
  USE ${DEMO_DB};
  SELECT * FROM test_ecf;"
# → la ligne '2025' apparaît côté slave
5) Pourquoi ces choix ?
  • Docker/Compose : reproductible, portable, isolement clair.
  • Binlog=ROW : fiabilité (triggers, fonctions non déterministes).
  • User ‘repl’ dédié : moindre privilège.
  • Volumes persistants : pas de perte au redéploiement.
  • Réseau dédié : isolation + DNS interne simple.
6) Sécurité & bonnes pratiques
  • Ne versionne pas .env ; fournis .env.example.
  • read_only=ON côté slave pour réduire le risque.
  • Sauvegardes : la réplication ≠ backup. Ajouter des dumps testés en restauration.
  • Surveillance : alerte si Slave_IO/SQL_Running passent à No.
  • GTID (optionnel) : facilite les bascules futures.
7) Dépannage (ce que je vérifierais)
  • IO/SQL = No : vérifier host/user/mdp, FILE/POS, réseau (docker exec mariadb_slave ping mariadb_master), SHOW SLAVE STATUS\G (Last_IO_Error / Last_SQL_Error).
  • Pas de données : confirmer log_bin actif, binlog_format=ROW, générer une nouvelle écriture sur le master.
  • Permissions : GRANT REPLICATION SLAVE + FLUSH PRIVILEGES;.