Skip to content

Docker mount guard

Boot Guard Docker et Dépendances de Montage Système

Incident type : Directory Shadowing et masquage de données

Lorsqu'un serveur de production redémarre, systemd lance les services en parallèle selon un graphe de dépendances. Par défaut, l'unité docker.service démarre dès que le réseau et le système de fichiers racine sont prêts.

Mais que se passe-t-il si vos volumes Docker pointent vers des partitions dédiées ou des pools ZFS montés sous /mnt/data ? 1. Docker démarre avant que ZFS ou le disque dur n'ait achevé son montage. 2. Le conteneur (ex: Nextcloud, Immich ou Transmission) démarre et constate que son répertoire de stockage /mnt/data/Photos ou /mnt/data/Nextcloud n'existe pas. 3. Docker crée alors automatiquement le dossier manquant sur le disque racine local (/) et le conteneur commence à y écrire des données. 4. Quelques secondes plus tard, le disque dur ou le pool ZFS finit enfin de se monter sur /mnt/data. 5. Problème silencieuse : Le point de montage ZFS recouvre et masque complètement le dossier local créé par Docker. Les données écrites pendant le démarrage deviennent invisibles, les conteneurs lèvent des erreurs de verrouillage de base de données, et l'espace disque du système racine se sature sans raison apparente.


1. Mécanisme de Résolution : Le Boot Guard

Pour éradiquer définitivement ce risque on peut installer un verrou logique : le démon Docker a interdiction de démarrer tant que chaque point de montage sous /mnt n'a pas été validé par le noyau.

graph TD
    Boot["Démarrage du Serveur Linux (systemd)"] --> StartServices["Initialisation des services système"]

    subgraph Storage_Mounts["Montages Disques et ZFS"]
        ZFS["Importation Pools ZFS (zfs-mount.service)"]
        FSTAB["Montages fstab (/mnt/data)"]
        ZFS --> MountReady["Points de montage actifs sur /mnt/data"]
        FSTAB --> MountReady
    end

    StartServices --> Storage_Mounts
    StartServices --> DockerRequest["Demande de démarrage docker.service"]

    subgraph Boot_Guard["Drop-in 10-mount-dependencies.conf"]
        SystemdUnits["Unités systemd : Requires et After"]
        PreExec["Script de contrôle : ExecStartPre"]
        Loop{"Point de montage actif et valide ?"}

        SystemdUnits --> PreExec
        PreExec --> Loop
        Loop -->|"Non (Attente du disque)"| Sleep["Attente active (sleep 1s)"]
        Sleep --> Loop
        Loop -->|"Oui (Point vérifié)"| Success["Code retour 0"]
    end

    DockerRequest --> Boot_Guard
    MountReady -.->|"Condition satisfaite"| Loop
    Success --> DockerStart["Lancement du démon dockerd"]
    DockerStart --> Containers["Démarrage des conteneurs applicatifs"]

2. Détection Dynamique des Points de Montage

Le playbook interroge la table des montages du noyau via findmnt pour identifier toutes les partitions physiques et réseaux actives sous /mnt (en excluant les dossiers temporaires ou les montages distants de sauvegarde remote-backup) :

findmnt -n -o TARGET -t ext4,ext3,ext2,xfs,btrfs,zfs,nfs,nfs4,cifs --list | grep "^/mnt/" | grep -v "^/mnt$" | grep -v "remote-backup"

Pour chaque cible détectée (par exemple /mnt/data), le playbook génère dynamiquement deux composants de protection.


3. Le Script de Garde : /usr/local/bin/docker-mount-check.sh

Ce script est invoqué par systemd immédiatement avant le binaire Docker (ExecStartPre). Il effectue une boucle active d'attente jusqu'à ce que la commande mountpoint -q confirme que le répertoire est un véritable point de montage du système de fichiers et non un simple dossier vide sur la racine :

#!/bin/bash
# Attente active de la disponibilité de tous les montages système
echo "Waiting for /mnt/data to be mounted..."
until mountpoint -q /mnt/data; do
  echo "/mnt/data not yet mounted, waiting..."
  sleep 1
done
echo "/mnt/data is mounted."

echo "All mount points are ready."
exit 0

Différence critique entre test -d et mountpoint -q

Un simple test Bash [ -d /mnt/data ] réussit même si le disque n'est pas monté (le dossier vide existe sur la racine). La commande système mountpoint -q vérifie le numéro de périphérique du système de fichiers (st_dev) et garantit que le répertoire correspond bien à un disque physique distinct rattaché.


4. Surcharge Systemd : /etc/systemd/system/docker.service.d/10-mount-dependencies.conf

Le playbook crée un fichier de surcharge (drop-in configuration) qui redéfinit les dépendances de docker.service :

[Unit]
# Dépendances fortes : Docker ne peut pas démarrer sans l'unité de montage
After=mnt-data.mount
Requires=mnt-data.mount

[Service]
# Script de contrôle exécuté avant dockerd
ExecStartPre=/usr/local/bin/docker-mount-check.sh

Explication des directives systemd :

  • Requires=mnt-data.mount : Indique à systemd que docker.service dépend strictement de l'unité de montage. Si le disque subit une panne matérielle et refuse de se monter, Docker ne démarrera pas, empêchant l'écriture de données fantômes.
  • After=mnt-data.mount : Impose l'ordre chronologique d'ordonnancement.
  • ExecStartPre : Offre une double sécurité au cas où un système de fichiers comme ZFS monterait ses datasets en dehors du cycle standard des .mount natifs de systemd.

5. Gestion de l'Idempotence et du Nettoyage

Si une machine de l'inventaire ne possède aucun montage personnalisé sous /mnt (par exemple un nœud de calcul ou un serveur dédié sans stockage local lourd), le playbook détecte l'absence de montages et supprime proprement l'override :

- name: Supprimer l'override du service Docker si aucun montage personnalisé
  file:
    path: "/etc/systemd/system/docker.service.d/10-mount-dependencies.conf"
    state: absent
  when: not (has_custom_mounts | bool)
  notify:
    - recharger systemd
Grâce à ce mécanisme automatisé, l'infrastructure est protégée contre toute perte ou désynchronisation de données lors des redémarrages inattendus ou des pannes de courant.


6. Playbook Ansible Prêt à l'Emploi (playbook-docker-mount-guard.yaml)

Voici le playbook autonome et complet, directement utilisable sur vos serveurs Debian, Ubuntu ou dérivés. Il inspecte automatiquement les partitions actives sous /mnt, installe le script de vérification et configure l'override systemd :

---
- name: Configuration du Boot Guard Docker pour les points de montage disques
  hosts: all
  become: true
  vars:
    docker_service_override_dir: /etc/systemd/system/docker.service.d

  tasks:
    - name: Vérifier la présence du binaire Docker
      ansible.builtin.stat:
        path: /usr/bin/docker
      register: docker_binary

    - name: Vérifier l'état de l'unité systemd docker.service
      ansible.builtin.systemd:
        name: docker.service
      register: docker_service_check
      failed_when: false

    - name: Interrompre si Docker n'est pas installé sur la machine cible
      ansible.builtin.meta: end_host
      when: not docker_binary.stat.exists or docker_service_check.status.LoadState | default('not-found') == 'not-found'

    # Détection dynamique des systèmes de fichiers physiques et réseaux montés sous /mnt
    - name: Identifier tous les points de montage sous /mnt
      ansible.builtin.shell: |
        findmnt -n -o TARGET -t ext4,ext3,ext2,xfs,btrfs,zfs,nfs,nfs4,cifs --list | grep "^/mnt/" | grep -v "^/mnt$"
      register: mount_points
      changed_when: false
      failed_when: false

    - name: Déterminer si des montages personnalisés sont présents
      ansible.builtin.set_fact:
        has_custom_mounts: "{{ mount_points.stdout_lines | default([]) | length > 0 }}"

    - name: Créer le dossier de configuration drop-in systemd pour Docker
      ansible.builtin.file:
        path: "{{ docker_service_override_dir }}"
        state: directory
        mode: '0755'
        owner: root
        group: root
      when: has_custom_mounts | bool

    # Génération du script de garde avec boucle active 'mountpoint -q'
    - name: Déployer le script de garde /usr/local/bin/docker-mount-check.sh
      ansible.builtin.copy:
        dest: /usr/local/bin/docker-mount-check.sh
        mode: '0755'
        owner: root
        group: root
        content: |
          #!/bin/bash
          # Généré par Ansible - Contrôle de présence des disques avant démarrage de Docker
          set -e
          {% for mount in mount_points.stdout_lines | default([]) %}
          echo "[Docker Boot Guard] En attente du montage de {{ mount }}..."
          until mountpoint -q "{{ mount }}"; do
            echo "[Docker Boot Guard] {{ mount }} n'est pas encore prêt, pause d'une seconde..."
            sleep 1
          done
          echo "[Docker Boot Guard] {{ mount }} est monté et opérationnel."
          {% endfor %}
          echo "[Docker Boot Guard] Tous les points de montage requis sont validés."
          exit 0
      when: has_custom_mounts | bool

    # Surcharge systemd liant Docker aux unités .mount correspondantes
    - name: Déployer l'override systemd 10-mount-dependencies.conf
      ansible.builtin.copy:
        dest: "{{ docker_service_override_dir }}/10-mount-dependencies.conf"
        mode: '0644'
        owner: root
        group: root
        content: |
          [Unit]
          # Dépendances générées automatiquement pour Docker
          {% for mount in mount_points.stdout_lines | default([]) %}
          {% set mount_unit = mount[1:] | replace('/', '-') + '.mount' %}
          After={{ mount_unit }}
          Requires={{ mount_unit }}
          {% endfor %}

          [Service]
          # Attente active bloquante avant exécution du démon dockerd
          ExecStartPre=/usr/local/bin/docker-mount-check.sh
      when: has_custom_mounts | bool
      register: docker_override_created
      notify:
        - recharger systemd
        - redémarrer docker

    # Nettoyage automatique si aucun montage personnalisé n'est présent
    - name: Supprimer l'override systemd si aucun montage n'est requis
      ansible.builtin.file:
        path: "{{ docker_service_override_dir }}/10-mount-dependencies.conf"
        state: absent
      when: not (has_custom_mounts | bool)
      register: docker_override_removed
      notify:
        - recharger systemd
        - redémarrer docker

    - name: Supprimer le script de garde si aucun montage n'est requis
      ansible.builtin.file:
        path: /usr/local/bin/docker-mount-check.sh
        state: absent
      when: not (has_custom_mounts | bool)

  handlers:
    - name: recharger systemd
      ansible.builtin.systemd:
        daemon_reload: true

    - name: redémarrer docker
      ansible.builtin.systemd:
        name: docker
        state: restarted
      when: docker_service_check.status.ActiveState | default('inactive') == 'active'

Comment l'exécuter :

ansible-playbook -i "mon_serveur," -u root playbook-docker-mount-guard.yaml
Le playbook est idempotent : si de nouveaux disques sont montés sous /mnt, il vous suffit de le réexécuter pour mettre à jour automatiquement le script de contrôle et le drop-in systemd.