Base de données dans Docker ou sur l'hôte ?
Faut-il conteneuriser PostgreSQL, MySQL ou Redis en production ? Découvrez pourquoi Docker est viable si vous maîtrisez les volumes, les sauvegardes et les limites mémoire.
La base de données doit-elle tourner dans Docker ou sur l'hôte ?
Faites tourner la base de données dans Docker. Pour une pile applicative sur un VPS unique, une instance conteneurisée de PostgreSQL, MySQL, MongoDB ou Redis est un choix de production standard, et le débat habituel à ce sujet est souvent mal posé. Un conteneur est un processus Linux isolé par des namespaces et des cgroups, et non une machine virtuelle ; il n'y a donc pas d'hyperviseur entre la base de données et le disque. Avec un bind mount ou un volume nommé local, les opérations de lecture et d'écriture s'effectuent directement sur le système de fichiers de l'hôte, exactement comme avec une installation via le gestionnaire de paquets.
Le coût réel est opérationnel. Quatre facteurs déterminent si cette configuration est viable ou vouée à l'échec : l'emplacement des données, les permissions sur le répertoire, la procédure de mise à jour majeure de version et la validation de vos sauvegardes par une restauration. Si ces points sont maîtrisés, le conteneur n'est qu'un détail technique. Dans le cas contraire, le conteneur sera le bouc émissaire de vos problèmes.
Cette logique s'applique à toute base de données serveur. Les exemples ci-dessous utilisent PostgreSQL, MySQL, MongoDB et Redis, et les spécificités propres à chaque produit sont mentionnées lorsqu'elles sont pertinentes.
Ce qu'un conteneur modifie réellement
Le chemin de stockage ne change pas, tant que vous en montez un. Le noyau, le cache de pages et le système de fichiers restent identiques.
Il existe un piège réel en termes de performance : le cas où vous ne montez rien. Sans volume, le répertoire de données est placé dans la couche inscriptible du conteneur, qui est un système de fichiers overlay superposé à l'image. Les écritures y sont plus lentes et toute la couche est supprimée lorsque le conteneur est supprimé. C'est la raison pour laquelle « ma base de données était vide ce matin ».
Ce qui change réellement :
- Le cycle de vie.
docker compose downdétruit le conteneur. Tout ce qui n'était pas dans un volume disparaît avec lui. - La version. Le tag de l'image est la version. Il n'y a pas de
apt upgradeà l'intérieur d'un conteneur de base de données qui survive audocker compose pullsuivant. - La comptabilité mémoire. Une limite cgroup est une barrière stricte imposée par le noyau, et la base de données ne sait pas qu'elle existe.
- L'utilisateur. Le processus s'exécute en tant qu'identifiant utilisateur numérique à l'intérieur du conteneur, qui peut ne rien posséder sur votre hôte.
L'emplacement des données détermine tout
Il existe deux bonnes options et une erreur courante.
- Un volume nommé :
pgdata:/var/lib/postgresql/data. Docker crée le répertoire dans/var/lib/docker/volumes/<project>_pgdata/_data, et l'entrypoint de l'image définit les permissions lors de la première exécution. C'est la solution par défaut. - Un bind mount :
/srv/appname/pg:/var/lib/postgresql/data. Vous choisissez le chemin, vous gérez donc vous-même les problèmes de permissions. - Aucun montage. Voir ci-dessus. Les données restent dans le conteneur.
Le compromis complet est un sujet en soi, et bind mounts contre volumes nommés le traite en détail. Pour une base de données, la version courte est : utilisez un volume nommé sauf si vous avez une raison précise de connaître le chemin sur l'hôte. Si vous utilisez un bind mount, placez-le dans un répertoire stable comme /srv/appname/pg plutôt qu'à l'intérieur du répertoire du projet où un git clean pourrait y accéder.
Une limite stricte : ne placez pas le répertoire de données d'une base de données sur un système NFS (network file system) ou tout autre montage réseau dont vous n'avez pas testé le comportement de verrouillage et de fsync. Les bases de données considèrent qu'un fsync réussi signifie que les octets sont écrits sur un stockage persistant. Si cette hypothèse est fausse, vous subirez une corruption de données qui n'apparaîtra que des semaines plus tard.
Fixez le nom du volume avant qu'il ne disparaisse
Compose nomme un volume <project>_<volume>, et le nom du projet utilise par défaut le nom du répertoire. L'identité du volume dépend donc du nom d'un répertoire, ce qui est une chose que l'on modifie souvent sans y penser.
Déplacez /srv/app vers /srv/app-old, ou renommez la clé pgdata dans le fichier compose, et le prochain docker compose up -d créera un volume vide flambant neuf. Postgres y initialise un nouveau cluster. Le conteneur est sain, l'application démarre, et toutes les tables ont disparu. La bonne nouvelle est que l'ancien volume est toujours présent sur le disque sous l'ancien nom.
docker volume ls
docker volume inspect app_pgdataFixez les noms pour éviter que cela ne se produise. Définissez explicitement le nom du projet et le nom du volume :
name: myapp
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
name: myapp_pgdataSi un volume orphelin contient déjà vos données, copiez-les après avoir arrêté la base de données :
docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start dbSi vous effectuez la copie pendant que la base de données est en cours d'exécution, vous obtiendrez une copie corrompue des fichiers en cours d'écriture. Arrêtez-la d'abord.
À qui appartient le répertoire de données
Les images officielles Postgres, MySQL et MongoDB exécutent leur serveur avec un identifiant utilisateur sans privilèges, généralement 999. Lorsque le conteneur démarre en root, le point d'entrée change le propriétaire du répertoire de données pour cet utilisateur, puis abandonne ses privilèges. C'est pourquoi un bind mount vide fonctionne généralement dès la première tentative.
Cela échoue dès que vous définissez user: dans le fichier compose, car le point d'entrée n'a alors plus les privilèges nécessaires pour corriger quoi que ce soit. Postgres l'indique explicitement :
initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permittedUn répertoire de données existant avec des permissions incorrectes génère un message différent. Il est utile de savoir le reconnaître, car la correction est chmod et non chown :
FATAL: data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL: Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).MongoDB sur un bind mount appartenant à root échoue au niveau du fichier de verrouillage (lock file) :
Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.La solution consiste à effectuer un chown du répertoire hôte vers l'identifiant numérique, et non vers un nom :
sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pgls -ldn affiche des nombres au lieu des noms, et il devrait indiquer 999 999. Le compte nommé postgres sur votre hôte et le compte nommé postgres à l'intérieur de l'image n'ont aucun lien : le noyau compare les nombres, et les noms sont résolus séparément de chaque côté. comment PUID et PGID mappent les utilisateurs hôtes dans un conteneur détaille correctement ce mappage. Avec Docker rootless ou le remappage d'espace de noms utilisateur (user namespace), les nombres changent à nouveau ; lisez donc les identifiants depuis le conteneur en cours d'exécution plutôt que de supposer qu'il s'agit de 999.
Les volumes nommés rendent toute cette section inutile lors de la première exécution, car Docker crée un répertoire vide et le point d'entrée en devient le propriétaire.
Mises à jour : montée de version d'un paquet par changement de tag d'image
Sur l'hôte, apt upgrade vous fait passer à une version mineure. Votre distribution ne fera pas basculer une version majeure de base de données à votre insu, et lorsque vous choisissez de le faire, les deux jeux de binaires peuvent être installés simultanément, ce qui est exactement ce dont pg_upgrade a besoin.
Dans un conteneur, le tag est la version ; une mise à jour consiste donc à modifier une ligne. Cela rend les mises à jour mineures triviales et les mises à jour majeures procédurales.
Modifiez postgres:16 en postgres:17, exécutez docker compose up -d, et le conteneur s'arrête immédiatement :
PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.Rien n'est endommagé. Les nouveaux binaires refusent de lire l'ancien schéma de catalogue sur disque, qui change entre les versions majeures. Remettez le tag à postgres:16 et il redémarre. Ce retour arrière est le seul véritable avantage des conteneurs en matière de mise à jour.
La procédure supportée est le dump et la restauration. PostgreSQL préfère que le dump soit effectué par le client le plus récent ; exécutez-le donc depuis la nouvelle image vers l'ancien serveur toujours en cours d'exécution sur le réseau compose :
docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sqlLe fichier doit faire au moins quelques dizaines de kilo-octets et se terminer par une ligne indiquant PostgreSQL database cluster dump complete. Un fichier de quelques centaines d'octets signifie que le dump a échoué et que vous vous apprêtez à supprimer un volume pour rien. Effectuez cette vérification avant de continuer :
docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sqlLes autres moteurs diffèrent :
- MySQL 8 met à jour son propre dictionnaire de données au démarrage ; un changement de tag mineur est donc généralement un simple redémarrage. Lisez les notes de version avant un saut entre deux séries de versions, et effectuez un dump dans tous les cas.
- MariaDB nécessite l'exécution de
mariadb-upgradeaprès le démarrage du serveur sur la nouvelle version. - MongoDB doit être mis à jour une version majeure à la fois, et après chaque étape, vous devez définir la version de compatibilité des fonctionnalités avant de poursuivre. Sauter une version signifie que
mongodrefusera de démarrer et journalisera une ligneUPGRADE PROBLEMmentionnantfeatureCompatibilityVersion. À partir de MongoDB 7.0, la commande nécessite un flag de confirmation explicite :db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true }). - Redis charge sans problème les anciens fichiers de snapshot, mais pas les plus récents ; une mise à jour est donc un redémarrage, et un retour en arrière peut échouer à charger les données.
La règle générale : un conteneur facilite le retour en arrière, mais ne simplifie pas la mise à jour elle-même.
Pourquoi mon conteneur de base de données s'arrête-t-il avec le code 137 ?
Cela signifie que le tueur OOM (Out of Memory) du noyau a arrêté le processus. Le code 137 correspond à 128 plus le signal 9.
docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20docker compose ps affiche Exited (137), la ligne inspect indique "OOMKilled": true, et le journal du noyau contient une entrée correspondante :
Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kBVoici le mécanisme, qui surprend souvent : PostgreSQL et MySQL dimensionnent leurs tampons en fonction de la mémoire totale rapportée par l'hôte. Une limite de cgroup ne modifie pas cette valeur pour eux. Sur un hôte de 16 Go avec une limite de 2 Go, la base de données planifie son activité comme si elle disposait de 16 Go, et le cgroup l'arrête bien avant que l'hôte lui-même ne soit sous pression. Une limite de mémoire seule ne suffit donc pas. Vous devez également indiquer à la base de données la mémoire dont elle dispose réellement :
- PostgreSQL : définissez
shared_buffers, et soyez attentif àwork_mem.work_memest alloué par opération de tri et par connexion ; une valeur généreuse multipliée par cinquante connexions est la cause habituelle d'un conteneur qui meurt en charge plutôt qu'au démarrage. - MySQL et MariaDB : définissez
innodb_buffer_pool_size, dont la valeur par défaut est 128M. Désactivezinnodb_dedicated_serverdans un conteneur, car son rôle est justement de se dimensionner à partir de la mémoire détectée de la machine. - MongoDB : définissez explicitement la taille du cache WiredTiger au lieu de le laisser deviner à partir de la mémoire de l'hôte.
- Redis :
maxmemoryest illimité par défaut, donc Redis croît jusqu'à ce que le cgroup l'arrête. Définissezmaxmemoryconfortablement en dessous de la limite du conteneur et choisissez unemaxmemory-policy.
Postgres rapporte également l'événement de son côté, et cette paire de lignes est ce que vous trouverez dans le journal :
LOG: server process (PID 123) was terminated by signal 9: Killed
LOG: terminating any other active sessions due to crash of another server processL'arrêt d'un processus backend force tous les autres à redémarrer, car la mémoire partagée peut être devenue incohérente. Il s'agit d'une tempête de connexions pour votre application, pas d'un événement silencieux. définir des limites de mémoire dans Docker Compose couvre la syntaxe et la différence entre mem_limit et la forme deploy.resources.
Rien de tout cela ne disparaît sur l'hôte. Le problème est simplement déplacé. Sans cgroup, la base de données entre en compétition avec tout le reste sur la machine, et le tueur OOM de l'hôte choisit une victime selon un score, qui peut être sshd. Une limite qui arrête la base de données de manière prévisible est plus facile à gérer qu'un OOM de l'hôte qui vous bloque l'accès.
Sauvegardes : dump à l'intérieur, sauvegarde à l'extérieur
Ne sauvegardez pas une base de données en cours d'exécution en copiant son répertoire de données. Une copie au niveau fichier effectuée pendant que le serveur écrit génère une copie incohérente (torn copy), et vous ne le découvrirez qu'au moment de la restauration.
Deux méthodes fiables : effectuez un dump avec l'outil natif de la base de données pendant qu'elle tourne et sauvegardez ce dump, ou arrêtez le conteneur et copiez le volume à froid.
docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVELe -T est important. Sans lui, docker compose exec peut attacher un terminal à la commande, et la couche terminal ajoute des retours chariot au flux de sortie. Un dump texte sera alors restauré avec des erreurs étranges et un dump binaire sera tout simplement corrompu. Cela échoue silencieusement au moment de la sauvegarde et bruyamment un mois plus tard.
--single-transaction fournit à mysqldump un snapshot cohérent des tables InnoDB sans verrouiller tout le serveur.
Ces commandes écrivent un fichier chacune. Ce ne sont pas un système de sauvegarde : il n'y a pas de rétention, pas de copie hors serveur, et pas de vérification. Confiez le répertoire de dump à un outil qui gère ces trois aspects, ce qui est l'objet de restic backups from a VPS. Sauvegardez /srv/backups, pas /var/lib/docker/volumes.
Ensuite, effectuez une restauration, car une sauvegarde que vous n'avez jamais restaurée n'est pas une sauvegarde :
docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'\dt devrait lister les tables de votre application. Un résultat vide, ou Did not find any relations., signifie que le dump n'est pas ce que vous croyez. Supprimez restore_test une fois terminé.
La commande qui supprime tout
docker compose down -v.
La commande down supprime les conteneurs et le réseau. L'option -v supprime également tous les volumes nommés déclarés dans ce fichier compose, ainsi que tous les volumes anonymes attachés à ces conteneurs. Il n'y a aucune confirmation et aucune possibilité d'annulation. C'est la méthode la plus courante de destruction d'une base de données auto-hébergée, et cela arrive généralement lors du dépannage d'un problème sans rapport, parce qu'une réponse sur un forum a conseillé de l'exécuter.
Quatre mesures permettent de réduire le rayon d'impact :
- Déclarez le volume de la base de données comme
external: true. Compose ne supprimera pas un volume dont il n'est pas propriétaire, donc-vne pourra pas l'atteindre. Vous le créez une seule fois avecdocker volume create myapp_pgdata. - Utilisez
docker compose stopetdocker compose startpour les redémarrages de routine. down contre stop dans Compose détaille ce que chaque commande supprime. - Conservez les dumps sur un chemin de l'hôte en dehors de tout volume géré par compose.
- Ne copiez jamais
-vprovenant d'une réponse de dépannage dans une stack qui contient des données importantes pour vous.
Ne publiez pas le port de la base de données
Cette ligne expose votre base de données sur l'internet public :
ports:
- "5432:5432"Elle lie le service à toutes les interfaces. Docker publie un port en réécrivant la destination du paquet avant que les règles d'entrée de votre pare-feu ne soient traitées. Comme les règles ufw se situent dans la chaîne input, ufw deny 5432 est sans effet. pourquoi les ports publiés par Docker contournent ufw détaille le parcours des paquets dans les chaînes.
Une application située dans le même projet compose accède à la base de données via le nom du service sur le réseau compose ; elle n'a donc pas besoin de port publié. Supprimez ce bloc. Si vous avez besoin d'un accès client depuis l'hôte, liez le service uniquement à l'interface de loopback :
ports:
- "127.0.0.1:5432:5432"Vérifiez quel service écoute réellement :
sudo ss -ltnp | grep 5432127.0.0.1:5432 est la configuration attendue. 0.0.0.0:5432 signifie que n'importe qui peut tenter de deviner votre mot de passe.
Où exécuter quoi
Une application sur un VPS. Utilisez un conteneur. Montez un volume nommé avec un identifiant fixe, sans port publié, avec une limite de mémoire adaptée aux paramètres de la base de données, et effectuez un dump quotidien vers un chemin hôte que restic récupère. Partez d'une installation Docker propre sur un VPS et conservez la stack dans un fichier compose que vous versionnez. L'avantage est réel : la version de la base de données devient une ligne révisable dans git.
Un hôte exécutant plusieurs services. Utilisez des conteneurs, avec une base de données par application, et non un serveur partagé pour l'ensemble. Un serveur partagé lie chaque application à un même calendrier de mise à jour, et une requête incontrôlée devient une panne pour tout le monde. Attribuez à chaque conteneur sa propre limite de mémoire afin qu'une requête défaillante soit contenue dans l'application qui l'a générée. Plusieurs petites instances Postgres consomment un peu plus de disque, mais nécessitent beaucoup moins de coordination.
La base de données est le cœur du produit. Exécutez-la sur l'hôte à partir du dépôt de paquets de l'éditeur, ou payez pour un service managé. pg_upgrade nécessite parfois l'installation simultanée de deux versions majeures des binaires, ce que les paquets permettent contrairement à une image à version unique. La réplication et la restauration à un instant T avec l'archivage des WAL (write ahead log) sont plus simples lorsque la base de données contrôle la machine et ses disques. Choisissez la solution éprouvée pour le système qui vous alertera à 03:00.
L'application est légère. Envisagez de ne pas utiliser de serveur de base de données. Une application web à écriture unique sur un VPS est souvent mieux servie par SQLite en production sur un VPS, où la sauvegarde se résume à un seul fichier et la mise à jour à une version de bibliothèque.
FAQ
Est-il sûr d'exécuter une base de données de production dans Docker ?
Oui, pour une pile applicative sur un serveur unique. Un conteneur est un processus Linux isolé par des namespaces et des cgroups. Avec un volume monté, la base de données écrit sur le système de fichiers de l'hôte exactement comme si elle était installée via un paquet. Les risques sont opérationnels plutôt que liés aux performances : un volume dont le nom n'est pas fixé, un bind mount appartenant au mauvais identifiant utilisateur, ou une restauration jamais testée, et docker compose down -v. Maîtrisez ces quatre points et le conteneur sera fiable. Passez à une installation sur l'hôte lorsque la base de données devient la charge de travail principale et que vous avez besoin de pg_upgrade, de réplication ou de restauration à un instant T.
Dois-je utiliser un bind mount ou un volume nommé pour les données de la base ?
Utilisez un volume nommé sauf si vous avez une raison spécifique de connaître le chemin sur l'hôte. Docker crée le répertoire et le point d'entrée de l'image définit les permissions au premier démarrage, ce qui évite les problèmes de droits. Fixez le volume avec un name: explicite ou marquez-le external: true, sinon renommer le répertoire du projet créera silencieusement un nouveau volume vide et une base de données vierge. Un bind mount est acceptable si vous appliquez un chown sur le répertoire hôte avec l'identifiant numérique de l'utilisateur utilisé par l'image, soit 999 pour les images officielles Postgres, MySQL et MongoDB. Vérifiez-le avec ls -ldn, car ls -l affiche le nom de l'utilisateur sur votre hôte pour ce numéro, ce qui n'a aucune signification à l'intérieur du conteneur.
Que supprime docker compose down -v ?
Cette commande supprime les conteneurs et le réseau comme un simple down, tandis que -v supprime en plus tous les volumes nommés déclarés dans le fichier compose ainsi que tous les volumes anonymes attachés à ces conteneurs. Cela inclut la base de données. Il n'y a pas de demande de confirmation ni de possibilité de récupération. Les volumes marqués external: true ne sont pas supprimés, ce qui est la raison principale pour marquer un volume de base de données comme externe. Pour un redémarrage courant, utilisez plutôt docker compose stop et docker compose start.
Comment mettre à jour PostgreSQL vers une nouvelle version majeure dans Docker ?
Il faut effectuer un dump et une restauration. Changer postgres:16 pour postgres:17 et redémarrer provoque FATAL: database files are incompatible with server avec une ligne DETAIL mentionnant les deux versions, car les nouveaux binaires ne peuvent pas lire l'ancien format du catalogue. Rien n'est endommagé : remettez l'ancien tag et le service redémarrera. Effectuez un pg_dumpall en utilisant le client de la nouvelle version contre l'ancien conteneur en cours d'exécution, vérifiez que le fichier se termine par PostgreSQL database cluster dump complete, puis lancez le nouveau tag sur un volume vide et chargez le dump. Les mises à jour mineures au sein d'une même version majeure nécessitent seulement un pull et un redémarrage.
Pourquoi mon conteneur de base de données s'arrête-t-il avec le code 137 ?
137 correspond à 128 plus le signal 9, ce qui signifie que le processus a été tué brutalement. Exécutez docker inspect <container> | grep -i oomkilled ; une valeur de true indique que le conteneur a atteint sa limite mémoire cgroup. La cause habituelle est que PostgreSQL et MySQL lisent la mémoire totale de l'hôte et ignorent la limite du conteneur ; ils planifient donc leur activité pour 16 Go alors qu'ils sont limités à 2 Go. Définissez shared_buffers et work_mem, ou innodb_buffer_pool_size, pour correspondre à la limite allouée au conteneur. Vérifiez journalctl -k pour trouver la ligne Memory cgroup out of memory correspondante et confirmer quel processus le noyau a sélectionné.