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 :
- est locale au moteur (Docker/Podman),
- est supprimée quand on supprime le conteneur (sauf cas particuliers),
- n’est pas conçue pour être un stockage durable.
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 :
-
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.
- Exemple :
-
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.
- Exemple :
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 :
- un volume,
- un bind mount,
- ou un stockage réseau (NFS, CIFS, iSCSI, Ceph, etc.) via un driver adapté.
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 :
- MySQL/Postgres refuse de démarrer
- messages du type
Permission denied,Can't create/write to file,initdb: could not create directory
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 :
- aligner UID/GID,
- corriger les droits,
- vérifier SELinux (section dédiée).
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 :
- identifier l’UID/GID attendu dans le conteneur :
docker run --rm mysql:8 bash -lc 'id mysql'
- 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 :
- Le montage est-il bien appliqué ?
- Le chemin source existe-t-il ?
- Le conteneur a-t-il les permissions ?
- Le système (SELinux/AppArmor) bloque-t-il ?
- Le driver de volume fonctionne-t-il ?
- Y a-t-il un conflit d’options (ro/rw, propagation, userns) ?
5.1 Vérifier les montages vus par Docker
docker inspect <container> --format '{{json .Mounts}}' | jq
Vous devez voir :
Type:bindouvolumeSource: chemin host ou volumeDestination: chemin dans le conteneurRW: true/false
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 :
- un volume vide monté sur un dossier non vide,
- un mauvais chemin source,
- un montage sur le mauvais conteneur,
- ou une erreur d’UID/GID empêchant l’écriture (donc pas de fichiers créés).
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 :
Permission deniedmalgréchmod 777(à éviter, mais typique),- logs applicatifs incompréhensibles,
- absence d’écriture dans le dossier monté.
6.1 Vérifier l’état SELinux
getenforce
sestatus
6.2 Solution Docker : options :Z et :z
:Z: label SELinux privé (recommandé pour un conteneur unique):z: label partagé (si plusieurs conteneurs doivent accéder au même bind mount)
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 :
- Utiliser un volume géré (souvent plus simple en rootless).
- Utiliser
podman unshare(Podman) pour manipuler les permissions dans l’espace userns. - Éviter de monter des répertoires sensibles du host.
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 :
- documentez la commande,
- utilisez un fichier de composition (docker compose) ou un script shell,
- vérifiez
.Mountsviadocker inspect.
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 :
- permissions,
- SELinux,
- montage ro,
- userns/rootless.
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
- Données : volume dédié (
/var/lib/...) - Config : bind mount en lecture seule si possible (
:ro) - Logs : idéalement stdout/stderr (collecte par le runtime), sinon volume séparé
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 :
- suppression du volume,
- corruption logique,
- ransomware,
- erreur humaine,
- panne disque.
Ayez :
- des dumps logiques (DB),
- des snapshots (si stockage le permet),
- des copies hors machine.
11.4 Tester la restauration (sinon la sauvegarde est théorique)
Plan minimal :
- restaurer sur une machine de test,
- vérifier intégrité,
- mesurer le temps.
12) Cas pratiques d’erreurs de montage et corrections
12.1 “Le conteneur démarre mais les données ne persistent pas”
Checklist :
- Le conteneur a-t-il un montage ?
docker inspect <container> --format '{{json .Mounts}}' | jq - L’application écrit-elle bien dans le chemin monté ?
docker exec -it <container> sh -lc 'ls -la /chemin/monté | head' - 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 :
- Préparer les permissions côté host,
- Utiliser un volume local géré,
- Configurer NFS sans root_squash (selon politique),
- Adapter l’image pour éviter
chownau runtime.
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 :
- absence de volume (données dans la couche writable),
- mauvais chemin monté (l’app écrit ailleurs),
- volume recréé par erreur,
- permissions/UID/GID incohérents,
- SELinux/AppArmor qui bloque,
- rootless/userns et mappings non anticipés,
- partage involontaire d’un même stockage entre plusieurs instances.
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.