Aller au contenu
Persister les données : volumes, montages et tmpfs

Persister les données : volumes, montages et tmpfs

200 · Pratiquer ⏱ 1 h dockerpostgresql

À la fin, vous saurez

  • Choisir entre volume nommé, montage de répertoire et tmpfs selon le besoin
  • Retrouver et récupérer des données restées dans un volume anonyme orphelin
  • Utiliser la syntaxe --mount et expliquer pourquoi elle est plus sûre que -v
  • Diagnostiquer et corriger une erreur de droits sur un montage
  • Sauvegarder et restaurer les données d'un conteneur de base de données
  • Rendre le système de fichiers d'un conteneur en lecture seule

Prérequis

Testé avec docker 29.8.1 postgres 18.6 ubuntu 24.04 , vérifié le 1 octobre 2026

Pourquoi

La leçon 7 l'a établi : tout ce qu'un conteneur écrit va dans sa couche inscriptible, qui disparaît avec lui. Or on supprime des conteneurs tout le temps : à chaque mise à jour d'image, à chaque changement de configuration, à chaque docker compose down. Pour une application sans état, c'est une bonne nouvelle. Pour une base de données, c'est une catastrophe annoncée.

Les incidents de données liés aux conteneurs suivent presque toujours le même scénario : quelqu'un recrée un conteneur pour changer une variable, et découvre que la base est vide. Parfois les données sont perdues ; souvent, elles sont encore sur le disque, dans un volume dont personne ne connaît le nom. Cette leçon vous apprend à placer les données au bon endroit dès le départ, et à les retrouver quand cela n'a pas été fait.

Les concepts

Trois façons de monter des données

Docker propose trois types de montage, qui greffent un répertoire sur l'arborescence du conteneur, par-dessus ce que l'image contient à cet endroit :

    flowchart LR
  subgraph Hôte
    V["/var/lib/docker/volumes/pgdata/_data<br/>(géré par Docker)"]
    B["/srv/signalements/config<br/>(n'importe quel répertoire)"]
    M["Mémoire vive"]
  end
  subgraph Conteneur
    CV["/var/lib/postgresql"]
    CB["/etc/signalements"]
    CT["/tmp"]
  end
  V -- "volume" --> CV
  B -- "bind mount" --> CB
  M -- "tmpfs" --> CT
  
VolumeMontage de répertoire (bind mount)tmpfs
Où sont les donnéesZone gérée par Docker (/var/lib/docker/volumes/)N'importe quel chemin de l'hôteEn mémoire
Créé parDocker (docker volume create ou au premier usage)VousDocker, à chaque démarrage
Survit au conteneurOuiOuiNon
Contenu initialCopié depuis l'image si le volume est videCelui du répertoire de l'hôte, qui masque l'imageVide
Usage typiqueDonnées d'application, bases de donnéesCode source en développement, fichiers de configurationFichiers temporaires, secrets en mémoire

Un volume est un répertoire géré par Docker, avec un nom, que l'on peut lister, inspecter, sauvegarder et supprimer avec docker volume. C'est le bon choix par défaut pour les données. Un volume peut être nommé (pgdata) ou anonyme (un nom aléatoire de 64 caractères hexadécimaux, créé automatiquement).

Un montage de répertoire (bind mount) expose un chemin précis de l'hôte dans le conteneur. C'est le même mécanisme que mount --bind sous Linux. Il dépend de l'arborescence de l'hôte, de ses droits, et l'hôte comme le conteneur voient chacun les modifications de l'autre en direct.

Un tmpfs est un système de fichiers en mémoire vive, vide à chaque démarrage, jamais écrit sur disque.

L'instruction VOLUME et les volumes anonymes

Une image peut déclarer, avec l'instruction VOLUME, qu'un répertoire contient des données. Nous l'avons vu à la leçon 6 avec PostgreSQL :

$ docker image inspect postgres:18 --format '{{json .Config.Volumes}}'
{"/var/lib/postgresql":{}}

Si vous ne montez rien à cet endroit, Docker crée automatiquement un volume anonyme. L'intention est bonne (ne pas écrire de grosses données dans la couche du conteneur), mais le résultat est trompeur : les données survivent au conteneur, dans un volume que rien ne relie plus à rien quand le conteneur est supprimé.

Les deux syntaxes : -v et --mount

-v pgdata:/var/lib/postgresql:ro
   └─┬──┘ └───────┬────────┘ └┬┘
  source    destination    options

--mount type=volume,src=pgdata,dst=/var/lib/postgresql,readonly

-v est concise mais ambiguë : si la source commence par / ou ., c'est un montage de répertoire ; sinon, c'est le nom d'un volume. --mount est verbeuse mais explicite : le type est écrit, chaque champ est nommé. Surtout, elles diffèrent sur un point décisif, que nous allons vérifier : quand la source d'un montage de répertoire n'existe pas, -v la crée (en root), --mount refuse.

En pratique

La perte de données, en direct

Lançons la base de données comme dans les leçons précédentes, sans nous occuper du stockage, et créons-y une table :

$ docker run -d --name pg -e POSTGRES_PASSWORD=essai postgres:18
$ docker exec pg psql -U postgres -qc "CREATE TABLE signalements(id serial, lieu text); INSERT INTO signalements(lieu) VALUES ('Rue des Lilas');"
$ docker exec pg psql -U postgres -tAc "SELECT count(*) FROM signalements"
1

On veut maintenant changer un paramètre : il faut recréer le conteneur.

$ docker rm -f pg
pg
$ docker run -d --name pg -e POSTGRES_PASSWORD=essai postgres:18
$ docker exec pg psql -U postgres -tAc "SELECT count(*) FROM signalements"
ERROR:  relation "signalements" does not exist
LINE 1: SELECT count(*) FROM signalements
                             ^

La table a disparu. Mais avant de supprimer le premier conteneur, nous avions noté le nom de son volume anonyme :

$ docker inspect pg --format '{{range .Mounts}}{{.Name}}{{end}}'
1f5e2721bb1a84130c5bb1fdf8bb7be451ea02254a269f1877ea927876bed19d

(commande lancée avant le docker rm -f). Ce volume existe toujours :

$ docker volume inspect 1f5e2721bb1a84130c5bb1fdf8bb7be451ea02254a269f1877ea927876bed19d --format '{{.Name}} {{.Mountpoint}}'
1f5e2721bb1a84130c5bb1fdf8bb7be451ea02254a269f1877ea927876bed19d /var/lib/docker/volumes/1f5e2721bb1a84130c5bb1fdf8bb7be451ea02254a269f1877ea927876bed19d/_data

docker rm -f supprime le conteneur, pas ses volumes. Le nouveau conteneur a simplement reçu un nouveau volume anonyme, vide. Les données sont donc récupérables, en montant l'ancien volume dans un conteneur de secours :

$ docker rm -f -v pg
$ docker run -d --name pg-sauvetage -e POSTGRES_PASSWORD=essai \
    -v 1f5e2721bb1a84130c5bb1fdf8bb7be451ea02254a269f1877ea927876bed19d:/var/lib/postgresql postgres:18
$ docker exec pg-sauvetage psql -U postgres -tAc "SELECT lieu FROM signalements"
Rue des Lilas

(docker rm -v supprime aussi les volumes anonymes du conteneur : c'est le cas du second conteneur, vide, dont on n'a pas besoin.) Sans le nom noté à l'avance, on aurait cherché parmi les volumes « pendants », qu'aucun conteneur n'utilise : docker volume ls --filter dangling=true, en les examinant un par un. Sur un serveur qui vit depuis des mois, ils peuvent être des dizaines. Et c'est dans cette liste que docker volume prune puise pour supprimer, sans retour possible (les volumes anonymes par défaut, tous avec -a).

Un volume nommé

La bonne pratique : un volume nommé pour toute donnée qui doit survivre.

$ docker volume create pgdata
pgdata
$ docker volume inspect pgdata
[
    {
        "CreatedAt": "2026-10-01T12:15:47+02:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Name": "pgdata",
        "Options": null,
        "Scope": "local"
    }
]
$ docker run -d --name pg -e POSTGRES_PASSWORD=essai -v pgdata:/var/lib/postgresql postgres:18
$ docker exec pg psql -U postgres -qc "CREATE TABLE signalements(id serial, lieu text); INSERT INTO signalements(lieu) VALUES ('Rue des Lilas');"
$ docker rm -f pg
$ docker run -d --name pg -e POSTGRES_PASSWORD=autre -v pgdata:/var/lib/postgresql postgres:18
$ docker exec pg psql -U postgres -tAc "SELECT lieu FROM signalements"
Rue des Lilas

Les données ont survécu à la recréation. Le pilote local stocke le volume dans un simple répertoire de l'hôte (Mountpoint). docker volume create est facultatif, -v pgdata:... crée le volume s'il n'existe pas, mais attention : une faute de frappe dans le nom crée silencieusement un volume vide, même si le bon volume existe. Seul Compose, avec un volume déclaré external: true, refuse un volume inexistant.

Le second conteneur a été lancé avec un autre mot de passe. Lisons ses premiers messages :

$ docker logs pg 2>&1 | head -3

PostgreSQL Database directory appears to contain a database; Skipping initialization

Le script d'entrée n'initialise la base que si le répertoire de données est vide. Toutes les variables d'initialisation (POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB) sont ignorées sur un volume existant. Un client qui se connecte par le réseau avec le nouveau mot de passe est refusé ; l'ancien fonctionne toujours :

$ docker run --rm -e PGPASSWORD=autre postgres:18 psql -h 172.17.0.3 -U postgres -tAc "SELECT 1"
psql: error: connection to server at "172.17.0.3", port 5432 failed: FATAL:  password authentication failed for user "postgres"
$ docker run --rm -e PGPASSWORD=essai postgres:18 psql -h 172.17.0.3 -U postgres -tAc "SELECT 'ancien mot de passe accepté'"
ancien mot de passe accepté

Pour changer le mot de passe d'une base existante, on passe par SQL (ALTER ROLE postgres PASSWORD '...'). Ce comportement est commun à presque toutes les images de bases de données (MySQL, MariaDB, MongoDB).

Le piège du chemin : PostgreSQL 18

Des milliers de tutoriels et de fichiers Compose montent le volume de PostgreSQL sur /var/lib/postgresql/data, le chemin des versions 17 et antérieures de l'image. Avec la version 18 :

$ docker run -d --name pg-ancien -e POSTGRES_PASSWORD=essai -v pg-ancien:/var/lib/postgresql/data postgres:18
$ docker ps -a --filter name=pg-ancien --format '{{.Status}}'
Exited (1) 3 seconds ago
$ docker logs pg-ancien 2>&1 | head -14
Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       See also https://github.com/docker-library/postgres/pull/1259

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data (unused mount/volume)

       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade" (which requires both
       versions).

Depuis la version 18, l'image range les données dans un sous-répertoire propre à la version majeure (/var/lib/postgresql/18/docker, la variable PGDATA vue à la leçon 6), et attend un montage unique sur /var/lib/postgresql. Ce découpage permet de faire cohabiter deux versions pendant une mise à niveau avec pg_upgrade. L'image a la bonne idée de refuser de démarrer plutôt que d'écrire les données ailleurs que dans le volume. Une image moins prudente aurait démarré, écrit dans sa couche inscriptible, et tout perdu à la recréation suivante.

Leçon générale : le chemin de montage fait partie du contrat de l'image. Lisez la documentation de l'image (ou docker image inspect pour voir ses Volumes et sa variable de répertoire de données) à chaque changement de version majeure.

Les montages de répertoire et le piège des droits

Pour étudier les montages de répertoire, construisons une petite image qui fait ce que fait toute application non-root : écrire un fichier dans son répertoire de données.

FROM alpine:3.22
RUN mkdir /donnees && chown 10001:10001 /donnees && echo "fichier livré avec l'image" > /donnees/LISEZMOI
USER 10001
CMD ["sh", "-c", "date > /donnees/trace && ls -ln /donnees"]
$ docker build -q -t ecrivain .
$ docker run --rm ecrivain
total 8
-rw-r--r--    1 0        0               28 Oct  1 10:16 LISEZMOI
-rw-r--r--    1 10001    0               29 Oct  1 10:16 trace

Sans montage, tout va bien : le répertoire appartient à l'UID 10001. Notez au passage le groupe 0 du fichier trace : USER 10001 sans groupe laisse le processus dans le groupe 0. Cela arrive quand l'UID n'a pas d'entrée dans le /etc/passwd de l'image : le groupe ne peut pas être déduit. Avec un utilisateur créé par useradd, comme à la leçon 8, le groupe vient de /etc/passwd. Dans le doute, écrivez explicitement USER uid:gid.

Avec -v et une source qui n'existe pas :

$ ls -d ./donnees-hote
ls: impossible d'accéder à './donnees-hote': Aucun fichier ou dossier de ce nom
$ docker run --rm -v ./donnees-hote:/donnees ecrivain
sh: can't create /donnees/trace: Permission denied
$ ls -ld ./donnees-hote
drwxr-xr-x 2 root root 4096 oct.   1 12:16 ./donnees-hote

Docker a créé le répertoire manquant en root, puisque c'est dockerd qui le crée. L'application non-root ne peut pas y écrire, et vous ne pouvez même pas le supprimer sans sudo. Une faute de frappe dans un chemin produit exactement ce résultat, en silence.

Avec --mount et une source qui n'existe pas :

$ docker run --rm --mount type=bind,src=./donnees-hote,dst=/donnees ecrivain
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /home/vous/labo/montages/donnees-hote

Une erreur claire, au lieu d'un répertoire fantôme. C'est la raison de préférer --mount dans les scripts et la documentation.

Avec un répertoire qui vous appartient :

$ mkdir donnees-hote
$ ls -ld donnees-hote
drwxrwxr-x 2 vous vous 4096 oct.   1 12:16 donnees-hote
$ docker run --rm --mount type=bind,src=./donnees-hote,dst=/donnees ecrivain
sh: can't create /donnees/trace: Permission denied

Toujours refusé. Le répertoire appartient à votre UID (1000 sur la machine de test), et le processus du conteneur tourne avec l'UID 10001. Le noyau compare des nombres ; il ne sait rien des noms d'utilisateur de part et d'autre. Remarquez aussi que le fichier LISEZMOI de l'image n'est plus visible : le montage masque le contenu de l'image à cet endroit.

Deux corrections possibles, selon le contexte :

  • En développement, lancer le conteneur avec votre propre UID, pour que les fichiers créés vous appartiennent :
$ docker run --rm --user $(id -u):$(id -g) --mount type=bind,src=./donnees-hote,dst=/donnees ecrivain
total 4
-rw-r--r--    1 1000     1000            29 Oct  1 10:16 trace
$ ls -l donnees-hote
total 4
-rw-r--r-- 1 vous vous 29 oct.   1 12:16 trace
  • Sur un serveur, préparer le répertoire pour l'UID de l'application : sudo install -d -o 10001 -g 10001 -m 0750 /srv/signalements/donnees. L'UID fait partie du contrat de l'image, au même titre que les chemins.

Les volumes nommés héritent du contenu et des droits de l'image

$ docker run --rm -v ecrits:/donnees ecrivain
total 8
-rw-r--r--    1 0        0               28 Oct  1 10:16 LISEZMOI
-rw-r--r--    1 10001    0               29 Oct  1 10:16 trace

Aucune erreur de droits, et LISEZMOI est présent. Quand un volume vide est monté sur un répertoire qui contient des fichiers dans l'image, Docker copie ce contenu dans le volume, avec les propriétaires et les droits, avant de démarrer le conteneur. Le volume hérite donc du chown 10001 fait dans le Dockerfile. C'est une raison de plus pour préférer les volumes aux montages de répertoire pour les données d'application. Cette copie n'a lieu qu'une fois, quand le volume est vide : les fichiers ajoutés à l'image dans une version ultérieure n'apparaîtront pas dans un volume déjà rempli.

tmpfs et système de fichiers en lecture seule

Un conteneur dont l'application ne doit rien écrire en dehors de ses volumes peut avoir une racine en lecture seule :

$ docker run --rm --read-only alpine:3.22 sh -c 'echo test > /tmp/x'
sh: can't create /tmp/x: Read-only file system

Beaucoup de programmes ont tout de même besoin d'un répertoire temporaire. On leur donne un tmpfs :

$ docker run --rm --read-only --tmpfs /tmp:rw,size=16m,mode=1777 alpine:3.22 \
    sh -c 'echo test > /tmp/x && df -h /tmp && grep " /tmp " /proc/mounts'
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    16.0M      4.0K     16.0M   0% /tmp
tmpfs /tmp tmpfs rw,nosuid,nodev,noexec,relatime,size=16384k,inode64 0 0

size=16m plafonne la mémoire consommée (sans cela, la limite est celle du noyau pour un tmpfs : la moitié de la mémoire de l'hôte, soit 15,4 Go sur la machine de test), mode=1777 donne les droits habituels de /tmp. Docker y ajoute d'office nosuid, nodev et noexec : on ne peut pas y exécuter de programme, ce qui complique la vie d'un attaquant qui voudrait y déposer un outil.

Sauvegarder et restaurer

Un volume se sauvegarde avec un conteneur jetable qui monte le volume en lecture seule et un répertoire de l'hôte pour l'archive. La base doit être arrêtée pendant la copie ; ici, on supprime le conteneur, le volume n'est alors plus utilisé par personne :

$ docker rm -f pg
pg
$ docker run --rm -v pgdata:/source:ro -v "$PWD":/sauvegarde alpine:3.22 \
    tar -czf /sauvegarde/pgdata.tar.gz -C /source .
$ ls -l pgdata.tar.gz
-rw-r--r-- 1 root root 4837771 oct.   1 12:16 pgdata.tar.gz

L'archive appartient à root : c'est le root du conteneur qui l'a écrite (ajoutez --user $(id -u):$(id -g) si le volume est lisible par votre UID, ou faites un chown ensuite). La restauration fait le chemin inverse, dans un nouveau volume :

$ docker run --rm -v pgdata-restaure:/cible -v "$PWD":/sauvegarde:ro alpine:3.22 \
    tar -xzf /sauvegarde/pgdata.tar.gz -C /cible
$ docker run -d --name pg-r -e POSTGRES_PASSWORD=x -v pgdata-restaure:/var/lib/postgresql postgres:18
$ docker exec pg-r psql -U postgres -tAc "SELECT lieu FROM signalements"
Rue des Lilas

Pour une base de données en service, la bonne méthode n'est pas la copie de fichiers, mais l'export logique fourni par le moteur, qui produit un instantané cohérent sans arrêt. Relançons la base sur son volume, puis exportons :

$ docker run -d --name pg -e POSTGRES_PASSWORD=essai -v pgdata:/var/lib/postgresql postgres:18
$ docker exec pg pg_dump -U postgres --format=custom > signalements.dump
$ ls -l signalements.dump
-rw-rw-r-- 1 vous vous 2735 oct.   1 12:16 signalements.dump
$ docker exec -i pg pg_restore -U postgres --list < signalements.dump | grep -i table
220; 1259 16385 TABLE public signalements postgres
3438; 0 16385 TABLE DATA public signalements postgres

pg_dump s'exécute dans le conteneur et écrit sur sa sortie standard, que le shell de l'hôte redirige vers un fichier (sans -t, pour ne pas corrompre le binaire avec des \r, leçon 5). Le format custom est compressé et permet une restauration sélective avec pg_restore. La seconde commande lit l'archive par l'entrée standard (-i) et en liste le contenu : c'est la vérification minimale qu'une sauvegarde est exploitable.

Sous le capot

Un volume ou un montage de répertoire est un bind mount du noyau (un tmpfs, lui, est un vrai montage d'un système de fichiers en mémoire). Pour un volume du pilote local, Docker crée le répertoire /var/lib/docker/volumes/<nom>/_data, puis runc le monte (avec mount --bind) à la destination demandée, dans le namespace de montage du conteneur, après avoir monté la racine overlay. Le montage se superpose donc à ce que l'image contient à cet endroit : c'est pourquoi le contenu de l'image est masqué.

La copie initiale du contenu de l'image dans un volume vide est faite par Docker avant le démarrage, et uniquement pour les volumes : un montage de répertoire n'est jamais modifié par Docker, ce qui est la garantie attendue quand on monte, par exemple, son répertoire de travail.

Les droits sont vérifiés par le noyau avec les UID et GID numériques du processus, exactement comme hors conteneur. Sans namespace user, l'UID 10001 du conteneur est l'UID 10001 de l'hôte : un fichier qu'il crée dans un montage appartient sur l'hôte à l'UID 10001, quel que soit le nom que l'hôte lui donne. Avec le mode rootless ou userns-remap (leçon 12), un décalage s'applique, et les fichiers apparaissent sur l'hôte avec des UID élevés (100000 et plus).

Les autres pilotes de volumes (NFS via les options du pilote local, pilotes de stockage en réseau des fournisseurs de cloud) montent un stockage distant au lieu d'un répertoire local, mais l'interface (docker volume create --driver ...) reste la même.

Pièges courants

Les données « disparues » à la recréation. Elles sont dans un volume anonyme orphelin. Cherchez avec docker volume ls --filter dangling=true, inspectez le contenu (docker run --rm -v <nom>:/v:ro alpine:3.22 ls -la /v), récupérez, puis passez à un volume nommé. Surtout, ne lancez pas docker volume prune avant d'avoir retrouvé vos données.

Le mauvais chemin de montage. Le volume est monté, mais pas là où l'application écrit. Symptôme : tout semble fonctionner, puis les données disparaissent à la recréation. Vérifiez où l'application écrit vraiment (docker diff montre les écritures dans la couche du conteneur, qui ne devraient pas exister pour des données).

Permission denied dans un montage. Comparez les UID numériques : docker exec <c> id d'un côté, ls -ln du répertoire de l'autre. Corrigez le propriétaire du répertoire ou l'utilisateur du conteneur, jamais avec un chmod 777, qui ouvre le répertoire à tous les utilisateurs de l'hôte.

Les variables d'initialisation ignorées. POSTGRES_PASSWORD, MYSQL_DATABASE et leurs équivalents ne servent qu'au premier démarrage sur un volume vide. Pour changer un mot de passe, passez par l'outil de la base.

Les montages sur Docker Desktop et sous SELinux. Sur macOS et Windows, les montages de répertoire traversent la frontière de la VM (leçon 3) : lents, et avec une gestion des droits différente. Sur Fedora ou RHEL, SELinux bloque l'accès aux répertoires montés tant qu'ils n'ont pas la bonne étiquette : les options :z (partagé entre conteneurs) ou :Z (privé) de -v la posent. N'utilisez jamais :Z sur un répertoire système comme /home ou /etc : vous changeriez son étiquette pour tout l'hôte.

Sécurité

  • Un montage de répertoire ouvre l'hôte au conteneur. Monter /, /etc, /var/run/docker.sock ou un répertoire personnel donne au conteneur un accès direct à ces fichiers, avec son UID (souvent root). Montez le strict nécessaire, et en lecture seule (readonly avec --mount, :ro avec -v) dès que l'écriture n'est pas requise.
  • Les sauvegardes sont des données. Une archive de volume ou un pg_dump contient toutes les données personnelles de la base : chiffrez-les, restreignez leurs droits, et stockez-les hors du serveur (stockage objet dans une autre région, avec une politique de rétention). Le cours Sauvegarde, restauration et PRA y est consacré.
  • --read-only et tmpfs réduisent ce qu'un attaquant peut faire dans un conteneur compromis : il ne peut ni modifier les binaires, ni déposer un outil exécutable dans /tmp.
  • Les secrets en tmpfs. Un fichier de secret monté dans un tmpfs n'est jamais écrit sur disque, ni dans la couche du conteneur, ni dans les sauvegardes de volumes. C'est le mécanisme des secrets de Kubernetes et de Docker Swarm. Attention : les secrets de Docker Compose, hors Swarm, sont de simples montages en lecture seule du fichier de l'hôte (leçon 11) ; le secret existe donc sur le disque de l'hôte.

En production

  • Des noms et des étiquettes. Nommez les volumes selon l'application et le rôle (signalements_pgdata), ajoutez des étiquettes (--label projet=signalements) pour pouvoir les filtrer, et documentez leur existence : un volume dont personne ne connaît le rôle finira supprimé.
  • Les bases de données dans des conteneurs ? C'est possible et courant sur un serveur unique ou en développement. En production à enjeu, on préfère souvent une base managée (Scaleway, comme les autres fournisseurs, propose PostgreSQL managé avec sauvegardes, réplication et mises à jour), ou un opérateur Kubernetes spécialisé (CloudNativePG). Le conteneur n'est pas le problème ; l'exploitation d'une base (sauvegardes testées, réplication, mises à niveau majeures) l'est.
  • Testez la restauration. Une sauvegarde jamais restaurée n'est qu'une hypothèse. Automatisez une restauration régulière dans un conteneur jetable, avec une requête de vérification, comme nous l'avons fait ici.
  • Kubernetes reprend les mêmes notions sous d'autres noms : volumes persistants (PersistentVolume, PersistentVolumeClaim) pour les volumes nommés, hostPath pour les montages de répertoire (déconseillé), emptyDir avec medium: Memory pour les tmpfs. Le problème des droits s'y pose de la même façon, et se résout avec securityContext.fsGroup.

Exercices

1. Choisir le bon montage. Pour chaque besoin, choisissez le type de montage et écrivez l'option --mount : (a) les données d'un serveur MariaDB ; (b) le code source d'une application en cours de développement, à recharger à chaud ; (c) un fichier de configuration nginx préparé sur l'hôte ; (d) le cache de compilation temporaire d'un outil, qui ne doit jamais toucher le disque.

Solution

(a) Volume nommé : --mount type=volume,src=mariadb_donnees,dst=/var/lib/mysql. (b) Montage de répertoire : --mount type=bind,src=./src,dst=/app/src. (c) Montage de répertoire en lecture seule : --mount type=bind,src=./nginx.conf,dst=/etc/nginx/nginx.conf,readonly. (d) tmpfs : --mount type=tmpfs,dst=/cache,tmpfs-size=268435456 (256 Mio).

2. Retrouver un volume orphelin. Lancez docker run -d --name essai -e POSTGRES_PASSWORD=x postgres:18, créez-y une table, puis docker rm -f essai. Sans avoir noté le nom du volume, retrouvez-le et prouvez qu'il contient votre table.

Solution
$ docker volume ls -q --filter dangling=true | xargs docker volume inspect -f '{{.CreatedAt}} {{.Name}}' | sort | tail -3
$ docker run --rm -v <nom>:/v:ro alpine:3.22 ls /v/18/docker
$ docker run -d --name verif -e POSTGRES_PASSWORD=x -v <nom>:/var/lib/postgresql postgres:18
$ docker exec verif psql -U postgres -c '\dt'

docker volume ls ne sait pas afficher la date de création ; on la demande à docker volume inspect, puis le tri désigne le volume le plus récent. Le contenu 18/docker confirme qu'il s'agit d'un répertoire de données PostgreSQL 18. Nettoyez ensuite avec docker rm -f verif puis docker volume rm <nom>, en vérifiant bien le nom.

3. Lecture seule de bout en bout (niveau 200). Lancez l'image signalements:1.0 de la leçon 8 avec une racine en lecture seule. Démarre-t-elle ? Si non, lisez l'erreur et ajoutez le strict nécessaire.

Solution
$ docker run --rm --read-only -p 127.0.0.1:8000:8000 signalements:1.0

Gunicorn a besoin d'un répertoire temporaire inscriptible pour les fichiers de pulsation de ses workers. En lecture seule, il échoue au démarrage :

FileNotFoundError: [Errno 2] No usable temporary directory found in ['/tmp', '/var/tmp', '/usr/tmp', '/app']

Le module tempfile de Python a essayé tous les emplacements habituels, puis le répertoire courant. La correction : --tmpfs /tmp:rw,size=16m. L'application n'écrit rien d'autre, ce qui confirme qu'elle est bien sans état.

4. Sauvegarde automatisée (niveau 200). Écrivez un script qui sauvegarde la base d'un conteneur pg avec pg_dump, horodate le fichier, vérifie qu'il est lisible par pg_restore, et ne garde que les sept dernières sauvegardes.

Solution
#!/usr/bin/env bash
set -euo pipefail
DEST=/srv/sauvegardes/signalements
FICHIER="$DEST/signalements-$(date +%Y%m%d-%H%M%S).dump"
umask 077
mkdir -p "$DEST"
trap 'rm -f -- "$FICHIER"' ERR
docker exec pg pg_dump -U postgres --format=custom > "$FICHIER"
docker exec -i pg pg_restore --list < "$FICHIER" > /dev/null
trap - ERR
ls -1t "$DEST"/signalements-*.dump | tail -n +8 | xargs -r rm --

umask 077 rend les sauvegardes lisibles par leur seul propriétaire. set -e arrête le script si pg_dump ou le contrôle échouent, et le trap ... ERR supprime alors le fichier incomplet, pour qu'une sauvegarde ratée ne compte pas parmi les sept conservées. Le contrôle par pg_restore --list échoue sur une archive tronquée ; pipefail protège la ligne de rotation, faite d'un enchaînement de commandes. Sans conteneur pg, le script s'arrête avec Error response from daemon: No such container: pg et ne laisse aucun fichier. En production, ajoutez une copie hors du serveur et une alerte en cas d'échec.

Récapitulatif

  • La couche d'un conteneur est jetable : les données vont dans un volume.
  • Volume nommé pour les données, montage de répertoire pour le code et la configuration en développement, tmpfs pour le temporaire et les secrets.
  • Une instruction VOLUME crée des volumes anonymes qui deviennent orphelins à la suppression du conteneur : les données ne sont pas perdues, mais difficiles à retrouver. Ne lancez pas docker volume prune à l'aveugle.
  • Préférez --mount : il refuse une source absente au lieu de créer un répertoire root.
  • Les droits se comparent en UID numériques ; les volumes nommés héritent du contenu et des droits de l'image, les montages de répertoire masquent l'image.
  • Le chemin de montage fait partie du contrat de l'image (PostgreSQL 18 : /var/lib/postgresql).
  • Pour une base en service, sauvegardez avec l'outil de la base (pg_dump), et testez la restauration.

Pour aller plus loin

  • La documentation Docker sur les volumes, en particulier les pilotes de volumes et le partage entre conteneurs.
  • Le chapitre Backup and Restore de la documentation PostgreSQL, qui explique pourquoi une copie de fichiers à chaud n'est pas cohérente.
  • La demande de fusion n° 1259 de l'image PostgreSQL, qui documente le changement de la version 18.
  • Leçon suivante : relier les conteneurs entre eux et les exposer, avec les réseaux Docker.

Sources