Si vous avez déjà redémarré un conteneur Postgres « sans état » et trouvé une base « fraîche » qui vous attendait comme un poisson rouge amnésique, vous avez rencontré
le plus grand mensonge du monde des conteneurs : que la base de données se soucie plus de votre YAML que du système de fichiers.
Postgres est ennuyeux de la meilleure façon — jusqu’à ce que vous l’exécutiez dans Docker avec de mauvais choix de stockage, des sauvegardes bâclées ou des mises à jour négligées.
Là, ça devient une histoire de détective où le coupable, généralement, c’est vous, hier.
Ce qui se passe réellement (et pourquoi Docker facilite les choses)
La plupart des incidents « perte de données Postgres dans Docker » ne sont pas mystiques. C’est la collision de deux systèmes tout à fait raisonnables :
Docker suppose que votre conteneur est jetable ; Postgres suppose que son répertoire de données est sacré.
Docker vous donne des primitives simples — images, conteneurs, volumes, montages bind, réseaux. Postgres vous donne des règles de durabilité strictes — WAL,
fsync, checkpoints, et une aversion violente pour les fichiers corrompus. Quand les gens se blessent, c’est généralement à cause d’une mauvaise hypothèse :
« Le conteneur est la base de données. » Non. Le conteneur est un wrapper de processus. La base de données est l’état sur disque plus le WAL plus les sauvegardes plus les règles que vous suivez.
Vous pouvez tout à fait exécuter Postgres dans Docker. Beaucoup de stacks de production le font. Mais il faut être explicite sur :
- Où se trouve le répertoire de données et comment il est monté.
- Comment fonctionnent les sauvegardes — et comment vous avez prouvé qu’elles restaurent.
- Comment faire des mises à jour sans réinitialiser accidentellement le cluster.
- Quels compromis de durabilité vous avez faits (parfois sans le savoir).
- Comment détecter la pression sur le stockage et le WAL avant que ça n’aboutisse à une interruption.
Le motif du piège est constant : un petit choix de commodité au jour 1 devient une panne à la semaine 12 parce que personne n’est revenu dessus une fois que « ça marchait ».
Faits intéressants & contexte historique
- Postgres a commencé au milieu des années 1980 (comme POSTGRES à Berkeley) et a conservé sa culture du « faire ce qu’il faut avec les données » même avec l’évolution des outils.
- Le Write-Ahead Logging (WAL) est le cœur de la durabilité de Postgres. Ce n’est pas optionnel en esprit, même quand des configs essaient de faire croire le contraire.
- Les volumes Docker sont gérés par Docker et résident par défaut sous le répertoire de données de Docker ; cela les rend portables lors du remplacement de conteneurs, mais pas en cas de perte de l’hôte.
- Les montages bind préexistent à Docker en tant que concept Unix. Ils sont puissants et transparents, ce qui explique aussi pourquoi on peut se tirer une balle dans le pied avec des problèmes de permissions et de chemins.
- « docker system prune » existe depuis des années et reste l’un des moyens les plus rapides de supprimer la mauvaise chose si vous traitez les volumes comme du « cache ».
- Les mises à niveau majeures de Postgres ne sont pas in-place par défaut ; elles nécessitent généralement dump/restore ou pg_upgrade, et les deux ont des angles vifs dans des conteneurs.
- Les systèmes de fichiers en overlay sont devenus courants avec les conteneurs ; ils sont excellents pour les images et les couches, et un endroit terrible pour stocker des bases de données à moins d’aimer les surprises d’E/S.
- Kubernetes a popularisé la maxime « pets vs cattle », qui fonctionne très bien jusqu’à ce que vous appliquiez la logique « cattle » au stockage de la base de données elle-même.
- Postgres dispose depuis longtemps d’une réplication logique solide (publication/abonnement) ; elle peut être un chemin de migration pratique hors d’une mauvaise configuration Docker sans interruption.
Votre modèle mental : container lifecycle vs. database lifecycle
Les conteneurs sont remplaçables. L’état de la base ne l’est pas.
Un conteneur est une instance en cours d’exécution d’une image. Tuez-le, recréez-le, replanifiez-le — pas de problème. C’est le but. Postgres ne se soucie pas des conteneurs.
Postgres se soucie de PGDATA (le répertoire de données), et suppose :
- Les fichiers restent en place.
- La propriété et les permissions restent cohérentes.
- fsync signifie fsync.
- Le WAL atteint le stockage stable quand il l’affirme.
Votre travail est de faire en sorte que la plomberie de stockage de Docker ne viole pas ces hypothèses. La plupart des problèmes se résument à :
- Mauvaise cible de montage : Postgres écrit dans les couches du système de fichiers du conteneur, pas dans un stockage persistant.
- Mauvaise source de montage : vous avez monté un répertoire vide et déclenché initdb par accident.
- Mauvaises permissions : Postgres ne peut pas écrire, donc il échoue ou se comporte étrangement avec la logique d’entrypoint.
- Mauvaise méthode de mise à niveau : vous avez créé un nouveau cluster et pointé les applis dessus.
- Mauvaises options de durabilité : des gains de performance qui acceptent silencieusement la perte de données en cas de crash.
Pourquoi « ça marchait sur mon laptop » est un piège
Sur un laptop, vous pouvez ne pas remarquer que vous avez stocké Postgres dans la couche du conteneur. Vous redémarrez le conteneur quelques fois ; les données restent.
Puis quelqu’un exécute docker rm ou la CI recrée les conteneurs, et les données s’évaporent parce qu’elles n’étaient jamais dans un volume.
Les conteneurs font en sorte que la mauvaise chose paraisse stable. Ils préserveront volontiers vos erreurs jusqu’au jour où ils ne le feront plus.
Scénarios de perte de données que vous pouvez reproduire (et prévenir)
Scénario 1 : Pas de montage de volume → les données vivent dans la couche du conteneur
Le classique. Vous lancez Postgres sans montage persistant. Postgres écrit dans /var/lib/postgresql/data à l’intérieur du système de fichiers du conteneur.
Redémarrer le conteneur conserve les données. Supprimer/recréer le conteneur les détruit.
Prévention : montez toujours un volume Docker ou un bind mount sur le répertoire PGDATA, et vérifiez le montage avec docker inspect.
Scénario 2 : Monter le mauvais chemin → les données vont ailleurs
L’image officielle utilise /var/lib/postgresql/data. Des gens montent /var/lib/postgres ou /data parce qu’ils l’ont fait ailleurs.
Postgres continue d’écrire sur le chemin par défaut. Votre stockage monté reste inutilisé, parfaitement vide, comme un parachute de secours laissé dans l’avion.
Prévention : inspectez les montages du conteneur et confirmez que la base écrit dans le système de fichiers monté. Vérifiez avec SHOW data_directory; dans Postgres.
Scénario 3 : Bind mount d’un répertoire vide → initdb s’exécute et « crée » un nouveau cluster
Beaucoup d’entrypoints initialisent un nouveau cluster quand PGDATA semble vide. Si vous avez monté par erreur un répertoire hôte tout neuf sur le vrai répertoire de données,
le conteneur voit le vide et lance initdb. Vous venez de créer une toute nouvelle base à côté de la vraie, et votre application se connecte au mauvais endroit.
C’est ainsi que la « perte de données » commence par « hein, pourquoi ma table manque ? » et finit par « pourquoi avons-nous écrit de nouvelles données dans le mauvais cluster pendant six heures ? »
Scénario 4 : « docker compose down -v » et assimilés → vous avez demandé à Docker de supprimer votre base
Docker facilite le nettoyage. Cela inclut les volumes. Si votre stockage Postgres est un volume nommé dans Compose, down -v le supprime.
S’il s’agit d’un volume anonyme, vous pouvez le supprimer via prune sans vous en rendre compte.
Prévention : traitez le volume Postgres comme un état de production. Protégez-le avec des conventions de nommage, des labels et un processus. Évitez les volumes anonymes pour les bases de données.
Scénario 5 : Exécuter Postgres sur un stockage en overlay → comportements étranges et risque accru de corruption
La couche écrivable de Docker est typiquement overlay2 (ou similaire). C’est acceptable pour les logs applicatifs. Ce n’est pas l’endroit où vous voulez des I/O aléatoires et des fsyncs intensifs.
La performance devient incohérente ; les latences picotent. En cas de crash ou de pression disque, les incidents de corruption deviennent plus plausibles.
Prévention : utilisez un volume ou un bind mount soutenu par un vrai système de fichiers sur l’hôte, pas la couche écrivable du conteneur.
Scénario 6 : « Tuning » de durabilité qui équivaut en réalité au mode perte de données
Désactiver fsync ou mettre synchronous_commit=off peut rendre les benchmarks héroïques.
Puis l’hôte plante, et la base manque des transactions récentes. Ce n’était pas « inattendu ». C’était l’accord.
Il existe des raisons légitimes de relâcher la durabilité (p.ex. dev éphémère, certains pipelines analytiques où perdre quelques secondes est acceptable).
Mais pour tout service exposé aux utilisateurs : ne le faites pas. Postgres est suffisamment rapide si vous le placez sur un stockage sensé et le configurez correctement.
Blague #1 : Désactiver fsync pour « accélérer Postgres » revient à enlever les freins pour « améliorer le temps de trajet ». Vous y arriverez plus vite, brièvement.
Scénario 7 : Le WAL remplit le disque → Postgres s’arrête, et la récupération devient compliquée
Le WAL est append-heavy. Quand le disque se remplit, Postgres ne peut plus écrire le WAL, et il s’arrête. Si vous avez aussi des paramètres de rétention mauvais ou pas de monitoring,
vous pouvez vous retrouver avec une instance bloquée et aucun chemin de récupération propre.
Prévention : surveillez l’utilisation disque là où résident PGDATA et le WAL. Définissez un max_wal_size sensé, configurez l’archivage si vous avez besoin de PITR, et conservez une marge.
Scénario 8 : Décalage de fuseau horaire/locale du conteneur et erreurs d’encodage → « perte de données » par mauvaise interprétation
Toutes les « pertes » ne sont pas des suppressions. Si vous initialisez un cluster avec une locale/encodage différent et comparez ensuite des dumps, vous pouvez constater un tri altéré,
des problèmes de collation ou du texte corrompu. Ce ne sont pas des octets manquants, mais ça peut ressembler à de la corruption.
Prévention : définissez explicitement la locale/encodage à l’initialisation, documentez-les et gardez-les stables lors des reconstructions.
Scénario 9 : Mise à niveau majeure en changeant le tag d’image → vous avez créé un cluster incompatible
Changer postgres:14 en postgres:16 et redémarrer avec le même volume ne « met pas à niveau » Postgres.
Postgres refusera de démarrer parce que le format du répertoire de données diffère. Sous pression, des gens « règlent » ça en supprimant le volume.
Ce n’est pas une mise à niveau. C’est de l’incendie criminel.
Prévention : utilisez pg_upgrade (souvent le plus simple avec deux conteneurs et une stratégie de volume partagé), ou la réplication logique, ou dump/restore — selon la taille et la tolérance d’indisponibilité.
Scénario 10 : Dérive des permissions (Docker rootless, changements UID/GID hôte) → Postgres ne démarre plus, quelqu’un « recrée » la BD
Les bind mounts héritent des permissions de l’hôte. Changez le mapping d’utilisateur hôte, déplacez des répertoires, passez en Docker rootless, ou restaurez depuis une sauvegarde avec une propriété différente,
et soudain Postgres ne peut plus accéder à ses propres fichiers. En panique, les équipes suppriment souvent le montage et « repartent à zéro ».
Prévention : standardisez la propriété et utilisez des volumes nommés quand possible. Si vous devez bind monter, fixez les attentes UID/GID et testez sur la même famille d’OS.
Playbook de diagnostic rapide
Quand Postgres-in-Docker se comporte mal, ne traînez pas. Commencez par les trois questions qui décident de tout : « Où sont les données, peut-il écrire, et est-ce durable ? »
Première étape : confirmer que vous regardez le bon cluster
- Vérifiez le montage PGDATA : le répertoire de données est-il soutenu par un volume/bind mount ?
- Vérifiez le chemin du répertoire de données : que rapporte Postgres comme
data_directory? - Vérifiez l’identité du cluster : regardez
system_identifieret la timeline ; comparez à ce que vous attendez.
Deuxième étape : vérifiez la pression évidente sur le stockage
- Disque plein : utilisation du système de fichiers hôte là où vit le volume.
- Ballonnement WAL : taille de
pg_walet slots de réplication. - Stalls I/O : latence et timing de fsync ; le CPU du conteneur peut sembler correct alors que le stockage meurt.
Troisième étape : vérifiez la durabilité et le statut de récupération après crash
- Sanité de la config : assurez-vous que
fsyncetfull_page_writesne sont pas désactivés en production. - Logs : boucles de crash recovery, « invalid checkpoint record », ou erreurs de permission.
- Événements kernel/système de fichiers : dmesg pour erreurs I/O ; ils expliquent souvent la « corruption aléatoire ».
Comment trouver rapidement le goulot d’étranglement
Si le symptôme est « lent », décidez s’il s’agit du CPU, de la mémoire, du contentieux de verrous ou des I/O de stockage. Avec Docker, le stockage est le suspect habituel, et les logs le disent souvent tôt.
Si le symptôme est « données manquantes », arrêtez d’écrire immédiatement et vérifiez que vous n’avez pas démarré un nouveau cluster par erreur.
Tâches pratiques : commandes, sorties et décisions
Voici les tâches que j’exécute réellement quand ça sent le roussi. Chaque item inclut (1) la commande, (2) ce que signifie la sortie, et (3) la décision à prendre.
Supposez que le conteneur s’appelle pg et que vous avez un accès shell à l’hôte.
Tâche 1 : Lister les conteneurs et confirmer quel Postgres tourne
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES IMAGE STATUS PORTS
pg postgres:16 Up 2 hours 0.0.0.0:5432->5432/tcp
Sens : Confirme quelle image est active et si elle a redémarré récemment. Un « Up 3 minutes » suspect coïncide souvent avec des problèmes de répertoire de données.
Décision : Si le tag d’image a changé récemment, considérez les upgrades comme le suspect principal. Si le conteneur a redémarré de façon inattendue, allez directement aux logs et vérifications des montages.
Tâche 2 : Inspecter les montages et vérifier que PGDATA est persistant
cr0x@server:~$ docker inspect pg --format '{{json .Mounts}}'
[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]
Sens : Vous voulez un montage qui cible le vrai répertoire de données. Si vous ne voyez aucun montage vers /var/lib/postgresql/data, vos données sont dans la couche du conteneur.
Décision : Si le montage est manquant ou pointe ailleurs, arrêtez et corrigez le stockage avant toute autre manipulation. Ne « redémarrez pas jusqu’à ce que ça marche ».
Tâche 3 : Confirmer que Postgres pense que son répertoire de données est l’endroit monté
cr0x@server:~$ docker exec -it pg psql -U postgres -Atc "SHOW data_directory;"
/var/lib/postgresql/data
Sens : Cela doit correspondre à la destination du montage. Si ce n’est pas le cas, vous écrivez où vous ne le souhaitiez pas.
Décision : En cas de discordance, corrigez les variables d’environnement ou les flags en ligne de commande, et assurez-vous que le chemin PGDATA attendu de l’image officielle est utilisé partout.
Tâche 4 : Vérifier si vous avez initialisé par accident un nouveau cluster (system identifier)
cr0x@server:~$ docker exec -it pg psql -U postgres -Atc "SELECT system_identifier FROM pg_control_system();"
7264851093812409912
Sens : L’identifiant système est en pratique l’identité du cluster. S’il a changé après un redeploy, vous n’êtes pas sur le même cluster.
Décision : Si l’identifiant est inattendu, arrêtez les écritures applicatives, localisez le volume/bind mount original, et restaurez la connectivité vers le bon répertoire de données.
Tâche 5 : Vérifier les logs du conteneur pour initdb ou erreurs de permission
cr0x@server:~$ docker logs --since=2h pg | tail -n 30
PostgreSQL Database directory appears to contain a database; Skipping initialization
2026-01-03 10:41:07.123 UTC [1] LOG: starting PostgreSQL 16.1 on x86_64-pc-linux-gnu
2026-01-03 10:41:07.124 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
Sens : « Skipping initialization » est bon. Si vous voyez « initdb: warning » ou « Database directory appears to be empty » de façon inattendue, vous avez monté un répertoire vide.
Si vous voyez « permission denied », c’est un problème de propriété sur un bind mount.
Décision : Initdb alors que vous ne l’attendiez pas est une alarme rouge. N’allez pas plus loin tant que vous n’avez pas expliqué pourquoi Postgres a cru que le répertoire était vide.
Tâche 6 : Identifier si votre volume est nommé ou anonyme
cr0x@server:~$ docker volume ls
DRIVER VOLUME NAME
local pgdata
local 3f4c9b6a7c1b0b3e8b8d8af2c2e1d2f9d8e7c6b5a4f3e2d1c0b9a8f7e6d5
Sens : Les volumes nommés (pgdata) sont plus faciles à protéger et à référencer. Les volumes anonymes se perdent facilement lors d’un nettoyage.
Décision : Pour les bases de données, utilisez des volumes nommés ou des bind mounts explicites. Si vous voyez des volumes anonymes attachés à Postgres, migrez-les avant le « jour du cleanup ».
Tâche 7 : Voir quels conteneurs utilisent le volume (éviter de supprimer le mauvais)
cr0x@server:~$ docker ps -a --filter volume=pgdata --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES STATUS IMAGE
pg Up 2 hours postgres:16
Sens : Si plus d’un conteneur utilise le même volume, vous pourriez avoir un accès multi-writer accidentel. C’est un risque de corruption.
Décision : Assurez-vous qu’une seule instance Postgres écrit dans un répertoire de données donné. Si vous avez besoin de HA, utilisez la réplication, pas un stockage partagé multi-writer.
Tâche 8 : Vérifier l’espace libre sur le système de fichiers hôte qui supporte Docker
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 450G 410G 40G 92% /
Sens : 92% est une zone dangereuse pour la croissance du WAL et les pics de vacuum. Les bases ne gèrent pas poliment les disques pleins.
Décision : Si vous êtes au-dessus de ~85–90% en production, planifiez un nettoyage ou une expansion immédiate. Puis mettez des alertes et des cibles de marge.
Tâche 9 : Vérifier la taille du répertoire WAL à l’intérieur du conteneur
cr0x@server:~$ docker exec -it pg bash -lc 'du -sh /var/lib/postgresql/data/pg_wal'
18G /var/lib/postgresql/data/pg_wal
Sens : Un WAL volumineux peut être normal sous forte activité d’écriture, mais une croissance soudaine signifie souvent des slots de réplication bloqués, un max_wal_size trop grand,
ou un archivage qui ne se vide pas.
Décision : Si le WAL gonfle, vérifiez immédiatement les slots de réplication et l’état de l’archiver avant que le disque ne soit plein.
Tâche 10 : Vérifier les slots de réplication (cause fréquente d’un WAL non borné)
cr0x@server:~$ docker exec -it pg psql -U postgres -x -c "SELECT slot_name, active, restart_lsn FROM pg_replication_slots;"
-[ RECORD 1 ]----------------------------
slot_name | analytics_consumer
active | f
restart_lsn | 0/2A3F120
Sens : Un slot inactif peut retenir le WAL indéfiniment si aucun consommateur ne l’avance.
Décision : Si le slot est inutilisé, supprimez-le. S’il est nécessaire, réparez le consommateur et confirmez qu’il avance. Ne vous contentez pas d’« augmenter le disque ».
Tâche 11 : Vérifier la santé de l’archiver si vous utilisez l’archivage WAL
cr0x@server:~$ docker exec -it pg psql -U postgres -x -c "SELECT archived_count, failed_count, last_archived_wal, last_failed_wal FROM pg_stat_archiver;"
-[ RECORD 1 ]-----------------------
archived_count | 18241
failed_count | 12
last_archived_wal | 0000000100000000000001A3
last_failed_wal | 0000000100000000000001A1
Sens : Les échecs signifient que votre chaîne PITR peut avoir des gaps. Cela signifie aussi que le WAL peut s’accumuler si l’archivage fait partie de votre plan de rétention.
Décision : Inspectez la commande d’archivage et le stockage. Si des échecs sont récents, considérez que les options de récupération sont compromises jusqu’à preuve du contraire.
Tâche 12 : Vérifier les réglages de durabilité (attraper le mode « benchmark » accidentel)
cr0x@server:~$ docker exec -it pg psql -U postgres -Atc "SHOW fsync; SHOW synchronous_commit; SHOW full_page_writes;"
on
on
on
Sens : Pour l’OLTP en production, c’est la base que vous voulez. Si fsync=off, vous avez explicitement accepté le risque de corruption en cas de crash.
Décision : Si ces options sont désactivées, activez-les et planifiez un redémarrage contrôlé. Puis expliquez aux parties prenantes pourquoi les réglages précédents étaient dangereux.
Tâche 13 : Confirmer que le conteneur n’est pas à court de mémoire (les OOM tuent ressemblent à des crashs aléatoires)
cr0x@server:~$ docker stats --no-stream pg
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a1b2c3d4e5f6 pg 85.3% 1.9GiB / 2.0GiB 95.0% 1.2GB/1.1GB 35GB/22GB 78
Sens : 95% d’utilisation mémoire avec un CPU élevé suggère de la pression. Si l’OOM killer de l’hôte intervient, Postgres redémarre et vous risquez une récupération plus longue.
Décision : Augmentez les limites mémoire, ajustez les paramètres mémoire de Postgres (shared_buffers, work_mem), et vérifiez que l’hôte n’est pas sur-souscrit.
Tâche 14 : Vérifier les erreurs d’I/O du système de fichiers sur l’hôte (la vérité peu glamour)
cr0x@server:~$ sudo dmesg -T | tail -n 20
[Sat Jan 3 10:22:11 2026] nvme0n1: I/O 128 QID 7 timeout, aborting
[Sat Jan 3 10:22:11 2026] EXT4-fs error (device nvme0n1p2): ext4_find_entry:1459: inode #262401: comm postgres: reading directory lblock 0
Sens : Si le kernel rapporte des timeouts I/O ou des erreurs de système de fichiers, cessez d’imputer Docker. Votre stockage est en train de faillir ou de mal se comporter.
Décision : Traitez cela comme un incident. Réduisez la charge d’écriture, prenez une sauvegarde si possible, et planifiez un basculement ou un remplacement du stockage.
Tâche 15 : Confirmer que des sauvegardes existent et sont restaurables (ne confondez pas « fichiers » et « sauvegardes »)
cr0x@server:~$ docker exec -it pg bash -lc 'ls -lh /backups | tail -n 5'
-rw-r--r-- 1 root root 1.2G Jan 3 02:00 pg_dumpall_2026-01-03.sql.gz
-rw-r--r-- 1 root root 1.1G Jan 2 02:00 pg_dumpall_2026-01-02.sql.gz
Sens : Vous avez des artefacts. Ce n’est pas la même chose qu’une restauration vérifiée.
Décision : Si vous ne pouvez pas prouver la restauration, planifiez un test de restauration. La production n’est pas le moment d’apprendre que vos dumps sont vides ou tronqués.
Tâche 16 : Exécuter un test rapide de restauration dans une base jetable
cr0x@server:~$ docker exec -it pg bash -lc 'createdb -U postgres restore_smoke && gunzip -c /backups/pg_dumpall_2026-01-03.sql.gz | psql -U postgres -d restore_smoke -v ON_ERROR_STOP=1'
SET
SET
CREATE TABLE
ALTER TABLE
Sens : S’il s’exécute sans erreurs et crée des objets, votre dump est au moins syntaxiquement utilisable. Ce n’est pas une validation complète, mais c’est mieux que rien.
Décision : Si des erreurs surviennent, cessez de supposer que vous avez des sauvegardes. Corrigez le pipeline et réexécutez jusqu’à ce que ce soit ennuyeux.
Trois mini-récits du monde de l’entreprise
Mini-récit #1 : La mauvaise hypothèse (l’incident « les conteneurs sont la persistance »)
Une équipe SaaS de taille moyenne a déplacé une application legacy dans Docker Compose pour que le dev local ressemble à la staging. Postgres est allé dans le même fichier Compose.
L’ingénieur qui a fait la migration avait de bonnes intentions et un délai, combinaison qui génère les pannes les plus intéressantes.
Ils ont testé les redémarrages avec docker restart. Les données étaient toujours là. Ils se sont congratulés et ont déployé le même Compose sur une petite VM de production.
Le service Postgres n’avait pas de volume configuré — juste le système de fichiers par défaut du conteneur.
Des semaines plus tard, ils ont remplacé l’hôte pour un patch de sécurité. Le nouvel hôte a démarré la stack Compose, et Postgres est apparu propre… parce qu’il était vide.
L’application s’est aussi lancée propre… et a commencé à recréer des tables, parce que l’outil de migration a vu « pas de schéma » et a fait son travail comme un bambin zélé avec des marqueurs permanents.
La réaction d’incident a été chaotique parce que l’équipe traitait initialement cela comme une corruption. Ils ont fouillé les réglages WAL et les versions du kernel.
La cause réelle était plus simple : ils n’avaient jamais persistant le cluster. Il n’y avait pas de « restauration depuis le disque ». Il y avait seulement « restauration depuis sauvegarde », et les sauvegardes étaient partielles.
L’action corrective qui a tenu : ils ont ajouté un volume nommé, fixé PGDATA explicitement, et écrit un script de préflight qui refusait de démarrer la production si le répertoire de données semblait fraîchement initialisé.
Ce script a énervé les gens exactement une fois, puis les a protégés d’une répétition de la même erreur lors d’un redeploy ultérieur.
Mini-récit #2 : L’optimisation qui a explosé (le « disque rapide » qui mentait)
Une autre organisation avait un conteneur Postgres bruyant et beaucoup d’écritures. Des pics de latence apparaissaient lors des heures de pointe, et les suspects habituels étaient blâmés :
autovacuum, verrous, plans de requête. Ils ont fait du tuning, obtenu des gains modestes, et voyaient toujours des stalls occasionnels.
Un ingénieur infra a proposé de déplacer le répertoire de données Docker vers un système de fichiers réseau « plus rapide » utilisé ailleurs pour des artefacts.
Il passait bien les benchmarks pour les écritures séquentielles et les gros fichiers. Postgres utilise des patterns fsync-heavy, des petites I/O aléatoires, et beaucoup de churn métadonnées.
Le déplacement semblait correct dans des tests synthétiques et même durant les premiers jours de production.
Puis est survenu le premier hic hôte réel. Un bref stall de stockage a poussé Postgres à logguer des warnings I/O puis à planter. Au redémarrage il est entré en crash recovery.
La récupération a pris beaucoup plus de temps que prévu, et les timeouts applicatifs sont devenus une interruption visible par les utilisateurs.
Le postmortem a été franc : le système de fichiers « rapide » était optimisé pour le débit, pas pour la consistance de latence et les sémantiques de durabilité.
Pire, son comportement sous charge ne correspondait pas à ce que Postgres attend quand il appelle fsync. Ils avaient échangé une performance courte contre une récupération fragile.
Ils sont revenus à un stockage local sur SSD et se sont concentrés sur les corrections ennuyeuses : dimensionner correctement l’instance, ajuster les checkpoints, et ajouter de la capacité en réplica.
La performance s’est améliorée, mais le vrai gain a été la stabilité — la récupération est redevenue prévisible.
Mini-récit #3 : La pratique ennuyeuse mais correcte qui a sauvé la mise (les drills de restauration)
Une grande équipe plateforme interne exécutait Postgres dans des conteneurs pour des dizaines de petits services. Rien d’exotique : volumes, tags figés, monitoring décent.
La partie qui paraissait excessive aux nouveaux arrivants était l’exercice trimestriel de restauration. Chaque trimestre, ils restauraient un sous-ensemble de bases dans un environnement isolé.
Ils vérifiaient le schéma, le nombre de lignes pour quelques tables clés, et faisaient des smoke tests applicatifs.
Un jour, un hôte a subi une panne de stockage. Un primaire Postgres est mort brutalement. Le réplica accusait un retard plus grand que souhaité, et il y avait de l’incertitude sur la disponibilité des WAL.
Personne n’a paniqué, ce qui n’est pas un trait de personnalité — c’est une procédure.
Ils ont basculé quand ils ont pu. Pour un service, ils ont dû restaurer depuis une sauvegarde sur un nouveau volume parce que l’état de réplication était douteux.
La restauration a été plus lente qu’un basculement mais a fonctionné exactement comme répété. Le service est revenu avec une fraîcheur des données acceptable, déjà convenue avec le business.
Lors du débrief, le « secret » de l’équipe n’était pas un outil intelligent. C’était la répétition. Ils avaient pratiqué la restauration tellement de fois que l’incident réel a ressemblé à un drill un peu plus pénible.
Erreurs courantes : symptômes → cause racine → fix
1) « Ma base s’est réinitialisée après un redeploy »
- Symptômes : Schéma vide, utilisateurs par défaut seulement, migrations applicatives relancées à zéro.
- Cause racine : Pas de volume persistant, ou montage d’un répertoire vide sur PGDATA entraînant initdb.
- Fix : Arrêtez les écritures, localisez le volume/bind mount original et rattachez-le. Ajoutez un volume nommé et un garde de démarrage qui vérifie l’identité attendue du cluster.
2) « Les données sont là, mais l’appli ne les trouve pas »
- Symptômes : psql montre les bonnes données ; l’appli voit des lignes/tables manquantes ; ou l’appli se connecte mais renvoie « relation does not exist ».
- Cause racine : L’appli se connecte à une autre instance Postgres/port, mauvais nom de base, mauvais réseau, ou mauvais volume attaché à un conteneur au nom similaire.
- Fix : Confirmez la chaîne de connexion, la résolution de nom du conteneur, les mappings de ports, et le
system_identifier. Étiquetez clairement volumes et conteneurs.
3) « Postgres ne démarre pas après une mise à niveau »
- Symptômes : Erreur fatale sur l’incompatibilité des fichiers de la base avec le serveur.
- Cause racine : Changement de version majeure sans pg_upgrade ni dump/restore.
- Fix : Revenez à l’ancien tag d’image pour restaurer le service. Planifiez une vraie mise à niveau : pg_upgrade dans un workflow contrôlé ou migration par réplication logique.
4) « Le WAL grossit jusqu’à remplir le disque »
- Symptômes :
pg_walénorme ; utilisation disque en hausse ; Postgres finit par s’arrêter. - Cause racine : Slot de réplication inactif, échecs d’archivage, ou retard de réplica avec contraintes de rétention.
- Fix : Identifiez les slots, supprimez ceux inutilisés, réparez les consommateurs, corrigez l’archivage, et ajoutez des alertes sur la croissance du répertoire WAL et la marge disque.
5) « Redémarrages aléatoires, parfois sous charge »
- Symptômes : Redémarrages de conteneur ; logs montrent une terminaison abrupte ; requêtes échouent de façon intermittente.
- Cause racine : OOM kills dus à des limites mémoire conteneurisées strictes, ou pression mémoire sur l’hôte.
- Fix : Augmentez la mémoire du conteneur, ajustez les paramètres mémoire de Postgres, et évitez la sur‑souscription de l’hôte. Confirmez avec
dmesget les stats du conteneur.
6) « Nous avons restauré le volume depuis un snapshot et maintenant Postgres se plaint »
- Symptômes : Crash recovery échoue, segments WAL manquants, erreurs d’état incohérent.
- Cause racine : Snapshot au niveau stockage pris sans coordination avec le système de fichiers/l’application ; le snapshot capture un point-in-time incohérent par rapport au WAL.
- Fix : Préférez des sauvegardes logiques ou des sauvegardes physiques coordonnées (pg_basebackup, archivage). Si vous snapshottez, gèlez les I/O ou utilisez des fonctionnalités de FS conçues pour des snapshots crash-consistents et testez la restauration.
7) « Permission denied au démarrage »
- Symptômes : Les logs Postgres mentionnent permission denied dans PGDATA ; le conteneur sort immédiatement.
- Cause racine : Bind mount possédé par le mauvais UID/GID ; problèmes d’étiquetage SELinux ; mismatch Docker rootless.
- Fix : Corrigez la propriété pour l’utilisateur Postgres, ajustez les options de montage, envisagez des volumes nommés pour éviter la dérive de permissions du FS hôte, et standardisez UID/GID.
8) « La performance est imprévisible : excellente puis horrible »
- Symptômes : Pics de latence, checkpoints lents, autovacuum bloqué, attentes I/O aléatoires.
- Cause racine : Stockage overlay, systèmes de fichiers réseau, voisins bruyants, checkpoints mal dimensionnés, ou WAL sur stockage lent.
- Fix : Mettez PGDATA sur un stockage local stable, ajustez les paramètres de checkpoint, surveillez les timings de fsync/checkpoint, et isolez la base des charges disque concurrentes.
Listes de contrôle / plan étape par étape
Checklist A : Le plan « je vais exécuter Postgres dans Docker pour de vrai »
- Choisir le stockage intentionnellement. Utilisez un volume nommé ou un bind mount vers un stockage dédié de l’hôte. Ne comptez pas sur la couche écrivable du conteneur.
- Épinglez les tags d’image. Utilisez
postgres:16.1(exemple) plutôt quepostgres:latest. « Latest » n’est pas une stratégie. - Verrouillez le nommage des volumes. Nommez-le comme un actif de production. Ajoutez des labels indiquant l’environnement et le service.
- Définissez explicitement PGDATA. Maintenez-le cohérent entre environnements et scripts.
- Configurez les sauvegardes dès le premier jour. Décidez : dumps logiques, sauvegardes physiques + archivage WAL, ou les deux.
- Testez les restaurations. Exécutez un test de restauration régulièrement, pas quand vous êtes déjà en difficulté.
- Alertez sur disque et croissance du WAL. Vous voulez être prévenu à 70–80%, pas à 99%.
- Conservez les valeurs par défaut de durabilité sauf si vous pouvez les justifier. Si vous changez des paramètres liés à fsync, documentez explicitement le budget de perte de données.
- Planifiez les mises à niveau comme des migrations. Les upgrades majeurs nécessitent une procédure, pas un simple changement de tag.
Checklist B : « Nous suspectons une perte de données » — étapes d’incident
- Arrêtez les écritures. Si l’appli écrit dans un cluster erroné ou neuf, chaque minute augmente les dégâts.
- Capturez les preuves. Logs du conteneur, sortie de
docker inspect,system_identifierde Postgres, et détails des montages. - Identifiez le bon répertoire de données. Trouvez le volume/bind mount contenant le cluster attendu (cherchez
PG_VERSION, fichiers de relation, et identifiant correspondant). - Confirmez l’état des sauvegardes. Quelle est la sauvegarde restaurable la plus récente ? Les archives WAL sont-elles intactes si vous avez besoin de PITR ?
- Récupérez le service en toute sécurité. Préférez rattacher le volume correct. Si vous devez restaurer, restaurez dans un nouveau volume et validez avant de basculer.
- Évitez la récidive. Ajoutez des gardes de démarrage, supprimez les volumes anonymes, et protégez les volumes de production des workflows de prune.
Checklist C : Chemin de mise à niveau qui ne ruine pas votre week-end
- Inventoriez les extensions. Assurez-vous que les extensions existent et sont compatibles avec la version Postgres cible.
- Choisissez une approche de mise à niveau : pg_upgrade (rapide, coordination nécessaire) vs dump/restore (simple, peut être lent) vs réplication logique (zéro/faible downtime, plus de pièces mobiles).
- Clonez les données de production. Utilisez un environnement de staging avec des données réalistes pour répéter la mise à niveau.
- Timing du cutover. Ayez un plan de rollback explicite : image ancienne + volume ancien intacts.
- Validez. Exécutez des smoke tests applicatifs, vérifiez les comptes de lignes sur les tables critiques, et comparez la performance des requêtes clés.
FAQ
1) Est-il « sûr » d’exécuter Postgres dans Docker en production ?
Oui, si vous traitez le stockage, les sauvegardes et les mises à niveau comme des éléments de première classe. Docker n’enlève pas les responsabilités liées aux bases ; il ajoute de nouvelles façons de les mal configurer.
2) Volume Docker ou bind mount — que devrais-je utiliser ?
Les volumes Docker nommés sont généralement plus sûrs opérationnellement : moins de surprises de permissions, plus faciles à référencer, et moins de couplage aux chemins hôtes.
Les bind mounts peuvent être excellents quand vous devez contrôler le système de fichiers et les snapshots, mais ils demandent une discipline stricte sur la propriété et la gestion des chemins.
3) Pourquoi ma base s’est-elle « réinitialisée » quand j’ai changé un fichier Compose ?
Souvent parce que le nom du service, le nom du volume ou le chemin de montage ont changé, provoquant la création d’un nouveau volume, ou parce que vous avez monté un nouveau répertoire hôte vide.
Postgres a alors initialisé un nouveau cluster.
4) Puis-je simplement mettre à jour en changeant le tag d’image ?
Versions mineures : généralement oui. Versions majeures : non. Les versions majeures nécessitent pg_upgrade, dump/restore ou migration par réplication logique. Les flips de tag sont la façon de découvrir une incompatibilité en runtime.
5) Quelle est la façon la plus rapide de confirmer que je suis sur les bonnes données ?
Interrogez system_identifier, vérifiez data_directory, et validez les montages avec docker inspect. Si ces éléments ne correspondent pas, ne faites confiance à rien d’autre.
6) Pourquoi le WAL est-il énorme alors que le trafic est normal ?
Causes courantes : un slot de réplication inactif, un réplica en retard, ou des échecs d’archivage WAL. La rétention du WAL n’est pas un nettoyage ; c’est un contrat avec des consommateurs.
7) « docker system prune » est-il sûr sur un hôte qui exécute des conteneurs Postgres ?
Ça peut l’être, mais seulement si vos règles opérationnelles sont strictes et que vous comprenez ce qu’il va supprimer. Si votre base utilise des volumes anonymes ou des volumes « inutilisés »,
prune peut supprimer la mauvaise chose. Traitez prune comme une tronçonneuse : utile, mais pas subtile.
8) Quels réglages de durabilité ne devrais-je jamais modifier en production ?
Ne désactivez pas fsync ou full_page_writes pour des systèmes OLTP. Soyez prudent avec synchronous_commit.
Si vous relâchez la durabilité, notez précisément la fenêtre de perte de données acceptée et obtenez l’accord des parties prenantes.
9) Comment éviter un initdb accidentel lors du montage du stockage ?
Utilisez un garde de démarrage : vérifiez la présence d’un fichier indicateur attendu, d’un PG_VERSION attendu, et (idéalement) d’un system_identifier attendu.
Refusez de démarrer si le répertoire semble tout neuf dans un environnement où il ne devrait pas l’être.
10) Pourquoi la performance empire-t-elle après un déménagement vers un stockage « meilleur » ?
Beaucoup de systèmes de stockage optimisent pour le débit, pas pour la consistance de latence et les sémantiques fsync. Les bases punissent la latence incohérente. Mesurez les timings de fsync et de checkpoint,
et choisissez un stockage qui se comporte bien sous pression.
Citation (idée paraphrasée) de Richard Cook : « Dans les systèmes complexes, les échecs sont normaux ; le succès exige une adaptation continue. » — Richard Cook, chercheur en opérations et sécurité.
Blague #2 : La seule chose plus persistante qu’un volume Docker est un ingénieur qui insiste pour ne pas avoir besoin de sauvegardes — jusqu’au moment où il en a besoin.
Conclusion : prochaines étapes à réaliser aujourd’hui
Si vous exécutez Postgres dans Docker, votre travail n’est pas de le rendre « container-native ». Votre travail est de le rendre ennuyeux. Stockage ennuyeux. Sauvegardes ennuyeuses. Mises à niveau ennuyeuses.
Le chemin excitant est celui qui finit par un schéma vide et une longue nuit.
- Auditez les montages : vérifiez que PGDATA est sur un volume nommé ou un bind mount délibéré, et que Postgres rapporte le data_directory attendu.
- Protégez les volumes : éliminez les volumes anonymes pour les bases et arrêtez d’utiliser
down -vdans tout workflow touchant la production. - Vérifiez les sauvegardes en restaurant : lancez un test de restauration cette semaine, puis planifiez-le régulièrement.
- Vérifiez les risques liés au WAL : inspectez les slots de réplication et l’état de l’archiver, et ajoutez des alertes pour la croissance du WAL et la marge disque.
- Rédigez le runbook de mise à niveau : épinglez les versions et choisissez une vraie méthode pour les upgrades majeurs avant d’en avoir besoin.