Mettre en place la redondance pour Mosquitto

Mettre en place la redondance pour Mosquitto

Dans le monde de la domotique, le broker MQTT Mosquitto est devenu un élément central pour faire communiquer nos équipements. Mais que se passe-t-il si votre serveur Mosquitto tombe en panne ? Vos appareils connectés se retrouvent orphelins ! J'ai eu le cas suite à un redémarrage et un fichier corrompu qui empêchait le démarrage de Mosquitto.

Dans ce tutoriel, nous allons mettre en place une solution de haute disponibilité pour Mosquitto en utilisant Keepalived. Le principe est simple : deux serveurs Mosquitto (un en Docker, un en binaire), une IP virtuelle, et un basculement automatique en cas de panne.

Keepalived for Linux
Keepalived provides robust High-Availability and Load Balancing features for Linux critical infrastructures
Cette mise en place repose sur un basculement de service via une IP virtuelle (Keepalived). Mosquitto ne disposant pas de clustering natif, le broker secondaire ne récupère pas l’état du broker principal lors du basculement. Les clients doivent donc se reconnecter automatiquement et certains messages en transit peuvent être perdus.
Cette approche reste toutefois parfaitement adaptée à un usage domotique ou personnel, où l’objectif principal est d’éviter une indisponibilité prolongée du broker MQTT.

Architecture de la solution

Ma solution repose sur quatre éléments principaux :

  • Un conteneur LXC avec Docker hébergeant Mosquitto en conteneur (serveur PRINCIPAL).
  • Un Raspberry Pi (qui me sert de unipi) avec Mosquitto en installation native (serveur SECONDAIRE).
  • Keepalived installé sur les deux serveurs pour gérer l'IP virtuelle.
  • Une IP virtuelle (VIP) : l'adresse IP unique à laquelle se connectent tous vos clients MQTT.

Le fonctionnement est transparent pour vos équipements : ils se connectent toujours à la même IP virtuelle, et en cas de défaillance du serveur principal, le Raspberry Pi prend automatiquement le relais en 2 à 5 secondes (tolérable dans nos installations).

Architecture hybride : Cette configuration combine le meilleur des deux mondes - Docker pour la flexibilité sur le serveur principal, et une installation native sur le Raspberry Pi pour la légèreté et la stabilité.

Dans cet article, je vais utiliser l'adresse IP 192.168.1.100 à adapter selon votre réseau. Cette adresse IP sera virtuelle donc, celle-ci ne remontera pas dans le routeur. Ainsi, si celle-ci ne doit pas faire partie de votre plage DHCP où être attribué à une IP fixe d'un équipement, sinon vous allez tout casser.


Installation sur les systèmes

Sur le serveur LXC Docker, installer keepalived (mosquitto sera vu après en docker).

sudo apt update
sudo apt install keepalived

Sur le Raspberry Pi, installer mosquitto et keepalived

sudo apt update
sudo apt install mosquitto mosquitto-clients keepalived

Vérifiez que Mosquitto est bien installé et démarré :

sudo systemctl status mosquitto

Si le service n'est pas actif, démarrez-le :

sudo systemctl enable mosquitto
sudo systemctl start mosquitto

Installer et configuration de Mosquitto sur le serveur principal (LXC Docker)

Création de la structure Docker

Sur le serveur LXC Docker, créez le dossier du projet :

mkdir -p ~/mosquitto && cd "$_"

Créez la structure de dossiers nécessaire :

mkdir -p config data log

Configuration de Mosquitto

Créez le fichier de configuration Mosquitto avec la commande nano config/mosquitto.conf :

# Configuration de base
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd

# Persistance des données
persistence true
persistence_location /mosquitto/data/

# Logs
log_dest file /mosquitto/log/mosquitto.log
log_dest stdout
log_type all
log_timestamp true
log_timestamp_format %Y-%m-%dT%H:%M:%S

Créez un fichier de mots de passe :

docker run -it --rm -v $(pwd)/config:/mosquitto/config eclipse-mosquitto:latest mosquitto_passwd -c /mosquitto/config/passwd votre_utilisateur

Entrez le mot de passe lorsqu'on vous le demande (Attention, vous en aurez besoin pour le Raspberry Pi).

Création du fichier Docker Compose

Créez le fichier compose.yaml avec la commande nano compose.yaml :

services:
  mosquitto:
    image: eclipse-mosquitto:latest
    container_name: mosquitto
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./config:/mosquitto/config
      - ./data:/mosquitto/data
      - ./log:/mosquitto/log
    healthcheck:
      test: ["CMD", "mosquitto_sub", "-t", "$$SYS/#", "-C", "1", "-i", "healthcheck", "-W", "3"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s

Lancez le conteneur :

docker compose up -d

Configuration de Mosquitto sur le serveur secondaire (un Raspberry Pi)

Configuration de Mosquitto

Sur le Raspberry, éditez le fichier de configuration avec la commande sudo nano /etc/mosquitto/mosquitto.conf :

# Configuration de base
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd

# Persistance des données
persistence true
persistence_location /var/lib/mosquitto/

# Logs
log_dest file /var/log/mosquitto/mosquitto.log
log_type all
log_timestamp true
log_timestamp_format %Y-%m-%dT%H:%M:%S

Créez le même utilisateur et mot de passe que sur le serveur principal :

sudo mosquitto_passwd -c /etc/mosquitto/passwd votre_utilisateur

Important : Utilisez exactement le même nom d'utilisateur et mot de passe que sur le serveur Docker.

Redémarrez Mosquitto pour appliquer les changements :

sudo systemctl restart mosquitto

Vérifiez que le service fonctionne :

sudo systemctl status mosquitto

Script de vérification pour Keepalived

Sur le serveur LXC Docker

Créez un script qui vérifiera si le conteneur Mosquitto est bien actif avec la commande nano /usr/local/bin/check_mosquitto_docker.sh :

#!/bin/bash
# Script de vérification du conteneur Mosquitto

# Vérifier si le conteneur est running et healthy
CONTAINER_STATUS=$(docker inspect --format='{{.State.Status}}' mosquitto 2>/dev/null)
CONTAINER_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' mosquitto 2>/dev/null)

if [ "$CONTAINER_STATUS" = "running" ]; then
    # Si healthcheck existe, le vérifier
    if [ -n "$CONTAINER_HEALTH" ]; then
        if [ "$CONTAINER_HEALTH" = "healthy" ] || [ "$CONTAINER_HEALTH" = "starting" ]; then
            exit 0
        fi
    else
        # Pas de healthcheck, on vérifie juste que le conteneur tourne
        exit 0
    fi
fi

exit 1

Rendez le script exécutable :

chmod +x /usr/local/bin/check_mosquitto_docker.sh

Testez le script :

sudo /usr/local/bin/check_mosquitto_docker.sh
echo $?

Le résultat doit être 0 si le conteneur fonctionne correctement.

Sur le Raspberry Pi

Créez un script qui vérifiera si le service Mosquitto est actif avec la commande sudo nano /usr/local/bin/check_mosquitto_native.sh :

#!/bin/bash
# Script de vérification du service Mosquitto natif avec authentification

# Identifiants MQTT
MQTT_USER="votre_utilisateur"
MQTT_PASS="votre_mot_de_passe"

# Vérifier si le processus mosquitto est en cours d'exécution
if systemctl is-active --quiet mosquitto; then
    # Vérifier que le broker répond avec authentification
    if timeout 2 mosquitto_sub -h localhost -t '$SYS/#' -C 1 -W 2 -u "$MQTT_USER" -P "$MQTT_PASS" >/dev/null 2>&1; then
        exit 0
    fi
fi

exit 1

Rendez le script exécutable :

sudo chmod +x /usr/local/bin/check_mosquitto_native.sh

Testez le script :

sudo /usr/local/bin/check_mosquitto_native.sh
echo $?

Le résultat doit être 0 si Mosquitto fonctionne correctement.


Configuration de Keepalived sur le serveur principal

Sur le serveur LXC Docker, créez le fichier de configuration Keepalived avec la commande nano /etc/keepalived/keepalived.conf :

# Script de vérification du conteneur Mosquitto
vrrp_script check_mosquitto_docker {
    script "/usr/local/bin/check_mosquitto_docker.sh"
    interval 3
    weight 2
    fall 2
    rise 2
}

# Instance VRRP
vrrp_instance VI_MQTT {
    state MASTER
    interface eth0  # Adaptez selon votre interface réseau
    virtual_router_id 51
    priority 101
    advert_int 1
    
    authentication {
        auth_type PASS
        auth_pass Mqtt2024SecurePass
    }
    
    virtual_ipaddress {
        192.168.1.100/24  # L'IP virtuelle
    }
    
    track_script {
        check_mosquitto_docker
    }
    
    # Notifications
    notify_master "/etc/keepalived/notify.sh MASTER"
    notify_backup "/etc/keepalived/notify.sh BACKUP"
    notify_fault "/etc/keepalived/notify.sh FAULT"
}

Points à adapter :

  • interface eth0 : remplacez par le nom de votre interface réseau (vérifiez avec ip a)
  • auth_pass : changez ce mot de passe par quelque chose de sécurisé
  • virtual_ipaddress : adaptez l'IP virtuelle selon votre réseau

Configuration de Keepalived sur le Raspberry Pi

Sur le Raspberry Pi, créez le fichier de configuration Keepalived avec la commande sudo nano /etc/keepalived/keepalived.conf :

# Script de vérification du service Mosquitto natif
vrrp_script check_mosquitto_native {
    script "/usr/local/bin/check_mosquitto_native.sh"
    interval 3
    weight 2
    fall 2
    rise 2
}

# Instance VRRP
vrrp_instance VI_MQTT {
    state BACKUP                # Serveur secondaire
    interface eth0              # Adaptez selon votre interface (souvent eth0 ou wlan0)
    virtual_router_id 51
    priority 100                # Priorité plus faible que le MASTER
    advert_int 1
    
    authentication {
        auth_type PASS
        auth_pass Mqtt2024SecurePass  # Doit être identique au MASTER
    }
    
    virtual_ipaddress {
        192.168.1.100/24
    }
    
    track_script {
        check_mosquitto_native
    }
    
    notify_master "/etc/keepalived/notify.sh MASTER"
    notify_backup "/etc/keepalived/notify.sh BACKUP"
    notify_fault "/etc/keepalived/notify.sh FAULT"
}

Sur un Raspberry Pi, l'interface peut s'appeler eth0 (câble) ou wlan0 (wifi). Vérifiez avec ip a.

Les différences avec le serveur principal sont :

  • state BACKUP au lieu de MASTER
  • priority 100 au lieu de 101
  • Script de vérification différent (check_mosquitto_native)

Script de notification

Sur les deux serveurs, créez un script simple de notification pour être informé des changements d'état avec la commande sudo nano /etc/keepalived/notify.sh :

#!/bin/bash
STATE=$1
HOSTNAME=$(hostname)
LOG_FILE="/var/log/keepalived-state.log"

case $STATE in
    "MASTER") 
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] Je suis maintenant MASTER" >> $LOG_FILE
        ;;
    "BACKUP") 
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] Je suis maintenant BACKUP" >> $LOG_FILE
        ;;
    "FAULT")  
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] Erreur détectée" >> $LOG_FILE
        ;;
    *)        
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] État inconnu : $STATE" >> $LOG_FILE
        ;;
esac

Rendez le script exécutable sur les deux serveurs :

sudo chmod +x /etc/keepalived/notify.sh

Démarrage de Keepalived et vérification du fonctionnement

Sur les deux serveurs, activez et démarrez Keepalived :

sudo systemctl enable keepalived
sudo systemctl start keepalived

Vérifiez le statut :

sudo systemctl status keepalived

Félicitation, tout est en place, mais nous allons quand même contrôler le fonctionnement.

Vérifier l'IP virtuelle

Sur le serveur (principal), vérifiez que l'IP virtuelle est bien active :

ip addr show

Vous devriez voir l'IP virtuelle 192.168.1.100 associée à votre interface réseau.

Sur le Raspberry Pi, vous ne devriez PAS voir l'IP virtuelle (il est en BACKUP) :

ip addr show

Connexion MQTT depuis un autre ordinateur

Depuis un autre ordinateur sur votre réseau, testez la connexion à l'IP virtuelle. Je réalise cela à l'aide de MQTT Explorer. Dans mon cas mon mosquitto principal étant déjà en service avant, je vois bien les messages de celui-ci.

Félicitation, tout doit être en service maintenant. Il vous reste sur l'ensemble de vos équipements à mettre l'adresse IP virtuelle comme adresse de brocker.

Simulation de panne du serveur principal

Arrêtez le conteneur Mosquitto sur le serveur LXC Docker :

docker stop mosquitto

Attendez quelques secondes (maximum 10 secondes avec notre configuration), puis vérifiez sur le Raspberry Pi que l'IP virtuelle a basculé :

ip addr show

L'IP virtuelle devrait à présent être sur le Raspberry Pi.

Consultez les logs pour voir le basculement :

sudo tail -f /var/log/keepalived-state.log

Testez à nouveau la connexion MQTT. Elle devrait toujours fonctionner (vous devrez peut-être vous reconnecter).

Redémarrez le conteneur sur le serveur principal :

docker start mosquitto

L'IP virtuelle devrait automatiquement revenir sur le serveur LXC Docker après quelques secondes (comportement par défaut).

Simulation de panne complète du serveur principal

Pour simuler une panne totale du serveur LXC Docker, arrêtez Keepalived :

sudo systemctl stop keepalived

Le Raspberry Pi devrait prendre le relais en moins de 5 secondes. Vos clients MQTT se reconnecteront automatiquement.


Configuration de vos clients MQTT

Désormais que votre infrastructure est prête, configurez tous vos clients MQTT (Home Assistant, Node-RED, Jeedom, Shelly, etc.) pour se connecter à l'IP virtuelle : 192.168.1.100.

Si vous souhaitez aller encore plus loin dans la redondance, vous pouvez ajouter un 3ᵉ serveur Mosquitto. Cela permet d'améliorer la tolérance aux pannes.

Architecture avec 3 nœuds

Serveur 1 (LXC Docker)     → MASTER  (priorité 101)
Serveur 2 (Raspberry UniPi) → BACKUP (priorité 100)
Serveur 3 (NAS/VM/Pi)       → BACKUP (priorité 99)

Principe de fonctionnement

  • En temps normal, le serveur 1 (LXC Docker) gère l'IP virtuelle
  • Si le serveur 1 tombe, le serveur 2 (Raspberry) prend le relais
  • Si les serveurs 1 et 2 sont tous les deux en panne, le serveur 3 prend le relais
  • Dès qu'un serveur de priorité supérieure revient, il reprend la main (comportement par défaut)

Configuration du 3ᵉ nœud

La configuration est identique aux autres serveurs, seuls changent :

  • Le state : toujours en BACKUP
  • La priority : 99 (inférieure aux deux autres)
  • L'installation : au choix Docker ou natif selon votre matériel

Exemple de configuration Keepalived pour le 3ᵉ nœud :

vrrp_instance VI_MQTT {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 99                  # Priorité la plus basse
    advert_int 1
    
    authentication {
        auth_type PASS
        auth_pass MqttHA2024SecurePass  # Identique aux autres
    }
    
    virtual_ipaddress {
        192.168.1.100/24
    }
    
    track_script {
        check_mosquitto_xxx  # Selon votre installation (docker ou native)
    }
}

Utiliser un NAS Synology peut-être possible, mais l'installation de Keepalived peut poser des soucis ou ne pas être persistant donc voilà pourquoi je n'ai pas pris en compte cette option dans mon installation

Pour la plupart des installations domotiques que nous avons, deux nœuds suffisent largement.


Utilisation avancée

Empêcher le retour automatique sur le principal

Si vous ne souhaitez pas que l'IP virtuelle revienne automatiquement sur le serveur principal après une panne, ajoutez cette ligne dans la configuration Keepalived des deux serveurs :

nopreempt

Et changez le state du serveur principal en BACKUP. Avec cette configuration, le serveur qui devient MASTER le reste jusqu'à sa prochaine panne.

Notifications par Telegram

J'ai rajouté un envoi vers Telegram pour connaitre l'état de mon mosquitto et pouvoir le dépanner au besoin. Modifier le script /etc/keepalived/notify.sh pour envoyer des notifications via Telegram, n'oublié pas de personnaliser vos paramètres du bot Telegram et du hostname de votre serveur principal.

#!/bin/bash
STATE=$1
HOSTNAME=$(hostname)
LOG_FILE="/var/log/keepalived-state.log"

# Configuration Telegram
TELEGRAM_BOT_TOKEN="votre_token_bot"
TELEGRAM_CHAT_ID="votre_chat_id"

# Déterminer si c'est le serveur principal ou secondaire
# Adaptez ces noms selon vos serveurs
if [[ "$HOSTNAME" == "lxc-docker" ]] || [[ "$HOSTNAME" == "votre-nom-serveur-principal" ]]; then
    IS_PRIMARY=true
else
    IS_PRIMARY=false
fi

# Message selon l'état
case $STATE in
    "MASTER") 
        MESSAGE="✅ Mosquitto - Le serveur $HOSTNAME est maintenant ACTIF"
        MQTT_STATUS="ACTIVE"
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] Je suis maintenant MASTER" >> $LOG_FILE
        ;;
    "BACKUP") 
        if [ "$IS_PRIMARY" = true ]; then
            MESSAGE="🚨 Mosquitto - Le serveur principal $HOSTNAME est EN PANNE"
            MQTT_STATUS="FAULT"
        else
            MESSAGE="⚠️ Mosquitto - Le serveur secondaire $HOSTNAME est en veille"
            MQTT_STATUS="STANDBY"
        fi
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] Je suis maintenant BACKUP" >> $LOG_FILE
        ;;
    "FAULT")  
        MESSAGE="🚨 Mosquitto - ERREUR CRITIQUE détectée sur le serveur $HOSTNAME"
        MQTT_STATUS="ERROR"
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] Erreur détectée" >> $LOG_FILE
        ;;
    *)        
        MESSAGE="❓ Mosquitto - État inconnu sur $HOSTNAME : $STATE"
        MQTT_STATUS="UNKNOWN"
        echo "$(date '+%Y-%m-%d %H:%M:%S') - [$HOSTNAME] État inconnu : $STATE" >> $LOG_FILE
        ;;
esac

# Envoi de la notification Telegram
curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
    -d chat_id="$TELEGRAM_CHAT_ID" \
    -d text="$MESSAGE" \
    -d parse_mode="HTML" > /dev/null 2>&1

État par MQTT

Modifier le script /etc/keepalived/notify.sh pour y ajouter cela sans oublier de modifier les valeurs de connexion de mosquitto.

# Configuration MQTT
MQTT_BROKER="192.168.1.100"  # IP virtuelle ou IP d'un autre broker
MQTT_PORT="1883"
MQTT_USER="votre_utilisateur"
MQTT_PASS="votre_mot_de_passe"
MQTT_TOPIC="mosquitto/status"

# Créer un payload JSON pour MQTT
MQTT_PAYLOAD=$(cat <<EOF
{
  "hostname":"$HOSTNAME",
  "server_type":"$SERVER_TYPE",
  "state":"$STATE",
  "status":"$MQTT_STATUS",
  "timestamp":"$(date '+%Y-%m-%d %H:%M:%S')"
}
EOF
)

# Publication MQTT
mosquitto_pub -h "$MQTT_BROKER" -p "$MQTT_PORT" \
    -u "$MQTT_USER" -P "$MQTT_PASS" \
    -t "$MQTT_TOPIC/$HOSTNAME" \
    -m "$MQTT_PAYLOAD" \
    -r  # Retained message pour garder le dernier état

Installation le paquet mosquitto_pub sur le serveur LXC Docker, celui-ci doit déjà être installé sur le Raspberry avec l'installation native de mosquitto.

apt install -y mosquitto-clients

Le script publiera sur mosquitto/status/"hostname" les statuts suivants :

  • ACTIVE → Le serveur gère actuellement l'IP virtuelle
  • FAULT → Le serveur principal est en panne
  • STANDBY → Le serveur secondaire est en attente
  • ERROR → Erreur critique détectée

Intégration dans Jeedom :

  1. Dans le plugin JMQTT, créez un équipement
  2. Abonnez-vous au topic mosquitto/status/#
  3. Utilisez ces infos dans vos scénarios

Intégration dans Home Assistant :

Modifier configuration.yaml pour y ajouter cette ligne

mqtt: !include mosquitto.yaml

Créer le fichier mosquitto.yaml dans le même répertoire que configuration.yaml et adapté vos hostname :

sensor:
  - name: "Mosquitto Principal"
    state_topic: "mosquitto/status/[hostname principal]"
    value_template: "{{ value_json.status }}"
    json_attributes_topic: "mosquitto/status/[hostname principal]"
    json_attributes_template: "{{ value_json | tojson }}"
    icon: mdi:server-network
    
  - name: "Mosquitto Secondaire"
    state_topic: "mosquitto/status/[hostname secondaire]"
    value_template: "{{ value_json.status }}"
    json_attributes_topic: "mosquitto/status/[hostname secondaire]"
    json_attributes_template: "{{ value_json | tojson }}"
    icon: mdi:server-network-off

binary_sensor:
  - name: "Mosquitto Principal Actif"
    state_topic: "mosquitto/status/[hostname principal]"
    value_template: "{{ value_json.status == 'ACTIVE' }}"
    payload_on: true
    payload_off: false
    device_class: connectivity
    
  - name: "Mosquitto Secondaire Actif"
    state_topic: "mosquitto/status/[hostname secondaire]"
    value_template: "{{ value_json.status == 'ACTIVE' }}"
    payload_on: true
    payload_off: false
    device_class: connectivity

Conclusion

Nous avons mis en place une solution de redondance assez simple pour Mosquitto. Cette solution peut être mise en place pour d'autre service IP. Il est bien sûr possible de partir avec l'installation principale et de simplement ajouter le secondaire et la partie Keepalived donc voici la documentation.

Cette solution est peu gourmande en ressource et peut permettre de maintenir une installation en service pour l'ensemble de la famille et vous permettre de dépanner tranquillement. Cette solution peut être pratique également pour les opérations de maintenance.

N'hésitez pas à partager vos déploiements ou venir chercher de l'aide avec la communauté sur le groupe Telegram ou bien en commentaire !