← Retour aux tutoriels

Conteneurs stateful : éviter la perte de données et corriger les erreurs de montage de volumes

conteneursstatefulvolumesdockerkubernetespersistance-des-donneesdebuggingdevopsstoragepermissions-linux

Conteneurs stateful : éviter la perte de données et corriger les erreurs de montage de volumes

Les conteneurs sont, par nature, éphémères. On peut les détruire et les recréer en quelques secondes. Cette philosophie fonctionne très bien pour des services stateless (API, workers, frontends), mais devient risquée dès qu’un service doit conserver des données : bases de données, files d’attente, moteurs de recherche, caches persistants, dépôts Git, etc.

Ce tutoriel explique en profondeur comment rendre un conteneur stateful fiable, comment éviter la perte de données, et comment diagnostiquer/corriger les erreurs de montage de volumes (permissions, SELinux, chemins, drivers, verrous, volumes “vides”, etc.). Les exemples utilisent principalement Docker (et un peu Podman quand c’est utile), avec des commandes réelles et reproductibles.


1) Comprendre où vont les données dans un conteneur

1.1 Système de fichiers en couches (overlay) et éphémérité

Un conteneur s’appuie sur une image (couches en lecture seule) + une couche en écriture (writable layer). Cette couche en écriture :

Vérifier l’espace utilisé et l’état :

docker system df
docker ps -a
docker inspect <container_id> | less

Si vous stockez des données dans /var/lib/mysql dans le conteneur sans volume, vous les perdrez à la suppression du conteneur.

1.2 Deux mécanismes : bind mounts vs volumes Docker

Il existe deux grandes familles de montages :

  1. Bind mount : vous montez un répertoire du host vers le conteneur.

    • Exemple : -v /srv/mysql:/var/lib/mysql
    • Avantages : transparent, facile à inspecter avec les outils du host.
    • Risques : permissions/SELinux, chemins, dépendance au host, erreurs de montage plus fréquentes.
  2. Volume Docker : Docker gère un espace de stockage (souvent sous /var/lib/docker/volumes/...).

    • Exemple : -v mysql_data:/var/lib/mysql
    • Avantages : plus portable, moins de surprises de permissions (souvent), gestion via docker volume.
    • Risques : moins “visible” si on ne connaît pas les commandes, confusion sur l’emplacement, driver/backup à comprendre.

Lister les volumes :

docker volume ls
docker volume inspect mysql_data

2) Règles d’or pour éviter la perte de données

2.1 Ne jamais compter sur la couche writable du conteneur

Si les données comptent, elles doivent être dans :

2.2 Vérifier que l’application écrit bien dans le chemin monté

Erreur classique : vous montez un volume sur /data, mais l’application écrit dans /var/lib/app. Résultat : vous croyez persister, mais vous ne persistez rien.

Dans le conteneur :

docker exec -it <container> sh
# ou bash selon l'image
mount | grep -E 'data|var/lib'
df -h
ls -la /data

Sur le host (bind mount) :

ls -la /srv/mysql

2.3 Comprendre l’initialisation “qui masque” le contenu

Quand vous montez un volume sur un répertoire non vide de l’image, le montage masque le contenu de l’image à cet emplacement.

Exemple : l’image contient déjà /var/lib/postgresql/data avec des fichiers par défaut. Si vous montez un volume vide dessus, vous ne verrez plus ces fichiers. Certaines images (Postgres officielle) gèrent cela via un script d’init qui copie des fichiers si le volume est vide. D’autres non.

Diagnostic :

docker run --rm -it postgres:16 bash -lc 'ls -la /var/lib/postgresql/data | head'

Puis avec volume :

docker volume create pgdata
docker run --rm -it -v pgdata:/var/lib/postgresql/data postgres:16 bash -lc 'ls -la /var/lib/postgresql/data | head'

3) Scénario complet : rendre PostgreSQL persistant (volume Docker)

3.1 Créer un volume et démarrer PostgreSQL

docker volume create pgdata
docker run -d --name pg \
  -e POSTGRES_PASSWORD='secret' \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16

Vérifier :

docker logs -f pg
docker exec -it pg psql -U postgres -c "SELECT version();"

Créer une table :

docker exec -it pg psql -U postgres -c "CREATE DATABASE demo;"
docker exec -it pg psql -U postgres -d demo -c "CREATE TABLE t(id serial PRIMARY KEY, v text);"
docker exec -it pg psql -U postgres -d demo -c "INSERT INTO t(v) VALUES('persistant');"
docker exec -it pg psql -U postgres -d demo -c "SELECT * FROM t;"

Supprimer/recréer le conteneur (sans supprimer le volume) :

docker rm -f pg
docker run -d --name pg \
  -e POSTGRES_PASSWORD='secret' \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16
docker exec -it pg psql -U postgres -d demo -c "SELECT * FROM t;"

Les données sont toujours là : persistance confirmée.

3.2 Sauvegarder un volume (backup) sans arrêter (ou presque)

Approche simple : lancer un conteneur temporaire qui lit le volume et crée une archive.

mkdir -p backups
docker run --rm \
  -v pgdata:/volume:ro \
  -v "$PWD/backups":/backup \
  alpine:3.20 sh -lc 'cd /volume && tar czf /backup/pgdata-$(date +%F).tar.gz .'
ls -lh backups/

Restauration (attention : cohérence applicative ; idéalement arrêter la DB ou faire un dump logique) :

docker rm -f pg
docker volume rm pgdata
docker volume create pgdata

docker run --rm \
  -v pgdata:/volume \
  -v "$PWD/backups":/backup \
  alpine:3.20 sh -lc 'cd /volume && tar xzf /backup/pgdata-YYYY-MM-DD.tar.gz'

Pour PostgreSQL, préférez souvent un dump logique :

docker exec -t pg pg_dumpall -U postgres > backups/pgdumpall.sql

4) Bind mount : puissance et pièges (permissions, SELinux, UID/GID)

4.1 Exemple : MySQL avec bind mount

Créer un répertoire sur le host :

sudo mkdir -p /srv/mysql
sudo chown -R 999:999 /srv/mysql
sudo chmod 700 /srv/mysql

Pourquoi 999:999 ? L’image MySQL officielle utilise souvent l’utilisateur mysql avec un UID/GID spécifique (souvent 999). Vérifiez :

docker run --rm mysql:8 bash -lc 'id mysql || true; getent passwd mysql || true'

Démarrer MySQL :

docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD='secret' \
  -v /srv/mysql:/var/lib/mysql \
  -p 3306:3306 \
  mysql:8

Logs :

docker logs -f mysql

4.2 Erreur typique : “Permission denied” sur le répertoire monté

Symptômes fréquents :

Diagnostic rapide :

docker logs mysql | tail -n 200
docker inspect mysql --format '{{json .Mounts}}' | jq
ls -ld /srv/mysql
sudo ls -la /srv/mysql | head

Corriger :

4.3 Cas subtil : l’application tourne en non-root, mais le host a des fichiers root

Si vous avez créé /srv/mysql en root et oublié chown, le conteneur (user mysql) ne pourra pas écrire.

Solution générale :

  1. identifier l’UID/GID attendu dans le conteneur :
docker run --rm mysql:8 bash -lc 'id mysql'
  1. appliquer sur le host :
sudo chown -R 999:999 /srv/mysql
sudo find /srv/mysql -type d -exec chmod 700 {} \;

5) Corriger les erreurs de montage de volumes : diagnostic systématique

Quand un montage “ne marche pas”, il faut distinguer :

5.1 Vérifier les montages vus par Docker

docker inspect <container> --format '{{json .Mounts}}' | jq

Vous devez voir :

5.2 Vérifier depuis l’intérieur du conteneur

docker exec -it <container> sh -lc 'mount | head -n 50'
docker exec -it <container> sh -lc 'df -hT'
docker exec -it <container> sh -lc 'ls -la <destination> | head'

Si le dossier est vide alors que vous attendiez des fichiers, c’est souvent :

5.3 Vérifier le chemin source (bind mount) côté host

ls -la /chemin/source
stat /chemin/source

Attention aux erreurs de frappe : Docker crée parfois le répertoire source s’il n’existe pas (selon la syntaxe et le contexte), ce qui peut donner l’impression que “ça marche” alors que vous montez un dossier vide inattendu.

5.4 Erreur : “invalid mount config for type bind: bind source path does not exist”

Cela arrive quand Docker ne crée pas automatiquement le chemin (cas fréquent avec --mount).

Exemple :

docker run --rm --mount type=bind,src=/nope,dst=/data alpine:3.20 ls /data

Correction :

sudo mkdir -p /nope
docker run --rm --mount type=bind,src=/nope,dst=/data alpine:3.20 ls -la /data

5.5 Erreur : “read-only file system” / montage en lecture seule

Vérifier si vous avez monté en :ro :

docker inspect <container> --format '{{range .Mounts}}{{println .Destination .RW}}{{end}}'

Ou si le FS est passé en lecture seule suite à une erreur disque côté host (dmesg) :

dmesg | tail -n 200
mount | grep ' ro[ ,]'

6) SELinux (Fedora/RHEL/CentOS) : la cause invisible la plus fréquente

Sur une machine avec SELinux en enforcing, un bind mount peut être correct en permissions Unix mais bloqué par SELinux. Symptômes :

6.1 Vérifier l’état SELinux

getenforce
sestatus

6.2 Solution Docker : options :Z et :z

Exemple :

sudo mkdir -p /srv/pg
sudo chown -R 999:999 /srv/pg

docker run -d --name pg \
  -e POSTGRES_PASSWORD='secret' \
  -v /srv/pg:/var/lib/postgresql/data:Z \
  -p 5432:5432 \
  postgres:16

6.3 Vérifier les labels

ls -Z /srv/pg | head

6.4 Audit SELinux (avc) pour confirmer

sudo ausearch -m avc -ts recent | tail -n 50
# ou
sudo journalctl -t setroubleshoot --since "10 min ago"

7) AppArmor (Ubuntu/Debian) et profils restrictifs

Sur Ubuntu, AppArmor peut restreindre certains accès. Docker utilise généralement un profil par défaut, mais des environnements durcis peuvent bloquer.

Vérifier AppArmor :

sudo aa-status
docker info | grep -i apparmor

Si vous suspectez AppArmor, regardez les logs kernel :

sudo dmesg | grep -i apparmor | tail -n 50

Désactiver un profil n’est pas une “solution” par défaut ; préférez adapter le profil ou changer de stratégie de montage. (Les détails dépendent fortement de la distribution et de la politique de sécurité.)


8) Problèmes de propriété (UID/GID) avec user namespaces, rootless Docker/Podman

8.1 Rootless : le conteneur n’est pas root sur le host

Avec Docker rootless ou Podman rootless, les UID/GID dans le conteneur sont mappés vers des UID/GID “subuid/subgid” sur le host. Un bind mount vers un répertoire système peut échouer.

Vérifier si vous êtes en rootless :

docker info | grep -i rootless
# Podman
podman info | grep -i rootless

Vérifier les mappings :

cat /etc/subuid
cat /etc/subgid

8.2 Symptôme : “permission denied” même avec chown

Dans ce cas, le chown attendu n’est pas celui que vous pensez. Approches possibles :

Exemple Podman (conceptuel mais commandes réelles) :

podman volume create pgdata
podman run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  docker.io/library/postgres:16

9) Volumes “vides” et confusion : pourquoi mes données ont disparu ?

9.1 Vous avez recréé un volume au lieu de réutiliser l’ancien

Un volume Docker est identifié par son nom. Si vous utilisez des noms générés ou différents, vous créez un nouveau volume vide.

Lister et repérer :

docker volume ls
docker volume inspect <nom>

9.2 Vous avez lancé le conteneur sans -v (ou avec une faute)

C’est bête, mais courant : un redéploiement manuel oublie le volume.

Pour éviter cela :

9.3 Vous avez monté sur le mauvais chemin dans le conteneur

Exemple : Postgres attend /var/lib/postgresql/data (ou variable PGDATA). Si vous montez ailleurs, vous persistez… mais la DB n’utilise pas ce chemin.

Vérifier la variable :

docker exec -it pg bash -lc 'echo $PGDATA; ls -la $PGDATA | head'

10) Techniques de diagnostic avancées

10.1 Inspecter l’emplacement réel d’un volume Docker sur le host

docker volume inspect pgdata --format '{{.Mountpoint}}'
sudo ls -la "$(docker volume inspect pgdata --format '{{.Mountpoint}}')" | head

Cela aide à confirmer que les fichiers existent bien.

10.2 Tester l’écriture directement dans le montage

Créer un fichier test :

docker exec -it <container> sh -lc 'echo test > /chemin/monté/_probe && ls -la /chemin/monté/_probe'

Puis vérifier côté host (bind mount) :

cat /chemin/source/_probe

Si ça échoue :

10.3 Surveiller les événements Docker

docker events --since 10m

Utile pour voir des erreurs au moment du démarrage.

10.4 Kernel logs et erreurs de FS

Si le disque du host a des soucis (ext4 remount ro, erreurs I/O), le conteneur “subit”.

sudo dmesg -T | tail -n 200
sudo journalctl -k --since "30 min ago" | tail -n 200

11) Bonnes pratiques de conception pour conteneurs stateful

11.1 Séparer données, config et logs

Exemple (générique) :

docker run -d --name app \
  -v app_data:/var/lib/app \
  -v "$PWD/app.conf":/etc/app/app.conf:ro \
  myimage:latest

11.2 Ne pas stocker de secrets dans le volume de données

Les sauvegardes de volumes partent souvent en archive. Séparez secrets (variables d’environnement, fichiers montés, gestionnaire de secrets) et données.

11.3 Stratégie de sauvegarde : volume ≠ backup

Un volume protège contre la suppression du conteneur, pas contre :

Ayez :

11.4 Tester la restauration (sinon la sauvegarde est théorique)

Plan minimal :


12) Cas pratiques d’erreurs de montage et corrections

12.1 “Le conteneur démarre mais les données ne persistent pas”

Checklist :

  1. Le conteneur a-t-il un montage ?
    docker inspect <container> --format '{{json .Mounts}}' | jq
  2. L’application écrit-elle bien dans le chemin monté ?
    docker exec -it <container> sh -lc 'ls -la /chemin/monté | head'
  3. Avez-vous recréé un autre volume ?
    docker volume ls

Correction typique : monter au bon chemin et réutiliser le même volume.

12.2 “Permission denied” avec bind mount sur Fedora

Correction typique : ajouter :Z.

docker run -d --name redis \
  -v /srv/redis:/data:Z \
  redis:7 redis-server --appendonly yes

12.3 “Device or resource busy” / verrous

Certaines bases posent des fichiers de lock. Si vous montez un répertoire partagé entre deux instances, elles se disputent.

Vérifier si deux conteneurs montent la même destination :

docker ps -q | xargs -I{} docker inspect {} --format '{{.Name}} {{range .Mounts}}{{.Name}}:{{.Destination}} {{end}}'

Solution : un volume par instance, ou un mécanisme de cluster prévu par l’application.

12.4 “Operation not permitted” lors d’un chown au démarrage

Certaines images font un chown au démarrage. En rootless ou sur FS particulier (NFS avec root_squash), cela peut échouer.

Solutions :


13) Commandes récapitulatives (anti-pannes)

Inspection et debug

docker logs <container>
docker inspect <container> | less
docker inspect <container> --format '{{json .Mounts}}' | jq
docker exec -it <container> sh
docker events --since 10m

Volumes

docker volume ls
docker volume create <nom>
docker volume inspect <nom>
docker volume rm <nom>

Sauvegarde simple d’un volume

docker run --rm -v <volume>:/volume:ro -v "$PWD":/backup alpine:3.20 \
  sh -lc 'cd /volume && tar czf /backup/backup.tar.gz .'

SELinux

getenforce
ls -Z /chemin
sudo ausearch -m avc -ts recent | tail
# option de montage : :Z ou :z

Conclusion

Rendre un conteneur stateful fiable repose sur une idée simple : les données doivent vivre hors du conteneur, dans un volume ou un bind mount correctement configuré. Les pertes de données viennent presque toujours d’un de ces problèmes :

En appliquant une méthode de diagnostic systématique (docker inspect, mount/df dans le conteneur, vérification host, logs kernel/SELinux), vous pouvez identifier rapidement la cause et corriger durablement.

Si vous me donnez un exemple concret (commande docker run, sortie de docker inspect --format '{{json .Mounts}}', distribution, et logs), je peux proposer une correction précise pour votre cas.