Vous avez fait ce qu’il fallait : vous avez monté un volume pour que vos données survivent aux redémarrages du conteneur.
Puis votre application démarre et s’effondre sur Permission denied comme si elle découvrait Linux pour la première fois.
Ce n’est pas un « Docker est buggé ». C’est Linux qui fait exactement ce que vous avez demandé — juste pas ce que vous vouliez.
Les conteneurs n’ont pas des « utilisateurs » au sens des noms. Ils ont des nombres. Et votre volume est aussi possédé par des nombres.
Quand ces nombres ne correspondent pas, vous obtenez le message d’erreur classique et professionnel : « non. »
Le vrai problème : les utilisateurs sont des nombres, les volumes sont des systèmes de fichiers
Docker n’a pas inventé les permissions. Il les a héritées. Un processus dans un conteneur s’exécute avec un UID et un ou plusieurs GID,
exactement comme n’importe quel autre processus Linux. Un volume (bind mount ou volume nommé) est porté par un véritable système de fichiers avec une véritable
propriété d’inode (UID/GID) et des bits de mode (rwx). Docker se contente de relier les deux.
Quand une image indique « exécuter en tant que appuser », ce que cela signifie réellement à l’exécution, c’est « exécuter en tant que UID 1001 (ou 999, ou 70, ou ce que le créateur de l’image a choisi). »
Pendant ce temps, votre répertoire hôte peut être possédé par l’UID 1000. Ou root. Ou un UID géré par LDAP qui n’existe pas dans le conteneur.
Linux ne tient pas compte des noms. Il compare des entiers. S’ils ne correspondent pas et qu’il n’y a pas de permission de groupe/ACL, l’accès échoue.
Volume nommé vs bind mount : la forme du piège de permission change
Deux styles de montage courants, deux modes d’échec légèrement différents :
-
Bind mount (
-v /host/path:/container/path) : le conteneur voit le répertoire hôte tel quel, y compris la propriété et les ACL.
Idéal pour le débogage ; impitoyable pour les dérives « marche sur ma machine ». -
Volume nommé (
-v myvol:/container/path) : Docker crée et gère un répertoire sous sa racine de données.
La propriété est toujours basée sur UID/GID, mais elle est maintenant créée par les valeurs par défaut de Docker/daemon et parfois « aidée » par les entrypoints d’image.
La vérité gênante : chmod 777 n’est pas une solution
Si vous avez déjà « résolu » cela avec chmod -R 777, vous n’avez pas réparé les permissions. Vous avez déclaré faillite.
Cela marche généralement, et cela revient généralement plus tard comme un incident de sécurité, une remarque de conformité ou une histoire de corruption mystérieuse.
L’approche correcte est ennuyeuse : faites correspondre le runtime UID/GID du conteneur avec la propriété du volume (ou inversement), intentionnellement et de manière reproductible.
Une citation à garder sur un post-it :
« L’espoir n’est pas une stratégie. »
— Gene Kranz
Le débogage des permissions est l’endroit où l’espoir vient mourir.
Playbook de diagnostic rapide
Quand vous êtes en astreinte, vous n’avez pas besoin d’une leçon. Vous avez besoin d’un chemin rapide vers la cause racine.
Voici l’ordre qui a tendance à faire disparaître la douleur rapidement.
Première étape : confirmer l’UID/GID effectif du processus
- Vérifiez l’utilisateur d’exécution du conteneur (
iddans le conteneur, ou inspectezUserdans la config). - Si c’est root et que ça échoue toujours, suspectez SELinux/AppArmor, NFS root-squash ou des montages en lecture seule.
Deuxième étape : vérifier le type de montage et le propriétaire/mode sur disque
- Est-ce un bind mount ou un volume nommé ?
- Sur l’hôte, inspectez la propriété (
stat) et les ACL éventuelles (getfacl).
Troisième étape : vérifier le système de fichiers et les couches de sécurité
- SELinux en mode enforcing ? Absence de labellisation
:Z/:zsur les bind mounts ? - NFS/CIFS avec root-squash ou identités mappées ?
- Docker rootless ou user namespaces qui déplacent les IDs ?
Puis : choisissez une stratégie de correction et rendez-la déterministe
- Meilleur défaut : exécuter le conteneur avec l’UID/GID de l’hôte et pré-créer les répertoires.
- Alternative : chown du volume une fois (avec précaution) via une étape d’initialisation, pas à chaque démarrage.
- Évitez : chown récursif perpétuel dans les entrypoints pour de gros jeux de données.
Blague n°1 : le chown récursif est la chose la plus proche d’une appli de méditation sous Linux. Il vous force à vous asseoir et réfléchir à vos choix.
Le correctif de volume qui règle vraiment le problème
Le correctif qui survit aux rebuilds, aux changements de nœud et à la créativité humaine est simple :
aligner l’UID/GID du processus du conteneur avec la propriété du volume.
Faites-le explicitement, pas par « ce que l’auteur de l’image a fait ».
Le modèle le plus fiable pour Docker Compose
Quand vous contrôlez le répertoire hôte et que vous utilisez des bind mounts, définissez user dans Compose sur l’UID/GID de l’utilisateur hôte,
et faites en sorte que le répertoire hôte appartienne à cet UID/GID.
cr0x@server:~$ id
uid=1000(cr0x) gid=1000(cr0x) groups=1000(cr0x),27(sudo),998(docker)
Décision : si votre service doit écrire dans un répertoire bind-monté appartenant à votre utilisateur de déploiement (UID 1000),
exécutez le conteneur en 1000:1000. Vous ne le rendez pas « moins sécurisé ». Vous le rendez prévisible.
Exemple de snippet Compose (conceptuel ; implémentez dans votre stack) :
- Hôte :
/srv/myapp/datapossédé par1000:1000, mode0750ou plus strict. - Conteneur : le processus s’exécute en
1000:1000.
Si vous devez utiliser un volume nommé
Les volumes nommés vont bien. Ils sont aussi assez opaques pour que les équipes commencent à deviner.
L’approche saine est :
- Créer le volume.
- Initialiser la propriété une fois, dans une étape contrôlée et auditable.
- Exécuter l’application comme un UID non-root qui correspond à cette propriété.
L’anti-pattern est de laisser le conteneur principal s’exécuter en root juste pour « réparer les permissions » au démarrage.
C’est comme ça que vous vous retrouvez avec un service root pour toujours parce que quelqu’un a peur d’y toucher.
Qu’en est-il du « juste chown » ?
Chowner peut être correct. Il peut aussi être catastrophique.
Sur de gros volumes, chown récursif parcourt tout le système de fichiers ; sur du stockage réseau, c’est un déni de service au ralenti.
L’astuce est de le faire une fois, et seulement si vous êtes sûr de cibler le bon chemin.
Modèles qui fonctionnent (et pourquoi)
Modèle A : Exécuter le conteneur en tant qu’UID/GID de l’hôte (meilleur choix par défaut pour les bind mounts)
C’est l’approche « faire plaisir à Linux ». Le système de fichiers a déjà une propriété. Faites correspondre le processus.
Cela évite les tempêtes de chown et fonctionne bien quand votre processus de déploiement possède déjà un compte de service stable.
Cela nécessite que l’application puisse s’exécuter en tant que non-root. La plupart des images modernes le peuvent.
Si une image insiste pour être root sans bonne raison, considérez cela comme un signe.
Modèle B : Initialiser la propriété du volume une fois (meilleur choix par défaut pour les volumes nommés)
Vous créez un petit conteneur one-shot dont la seule tâche est de créer des répertoires et de définir propriété/ACL,
puis vous exécutez l’appli comme un utilisateur non privilégié normal.
Bien fait, c’est déterministe. Mal fait, c’est un piège avec des étapes supplémentaires. La différence est :
vous ciblez des chemins exacts, évitez le chown récursif sauf si nécessaire, et consignez ce que vous avez fait.
Modèle C : ACL au lieu de la propriété (idéal quand plusieurs UIDs doivent écrire)
Parfois vous avez des sidecars ou plusieurs conteneurs écrivant sur le même montage (agent de sauvegarde, expéditeur de logs, appli).
La propriété ne peut pas satisfaire tout le monde à moins de forcer tout le monde à partager un UID (ce qui devient encombrant).
Les ACL vous permettent d’accorder l’écriture à des UID/GID supplémentaires sans rendre le répertoire accessible à tous.
Modèle D : Utiliser la propriété de groupe + répertoires setgid (Unix classique, toujours efficace)
Si plusieurs processus doivent créer des fichiers dans le même répertoire, rendez-le inscriptible par le groupe et définissez le bit setgid sur le répertoire.
Les nouveaux fichiers héritent du groupe du répertoire. C’est une de ces fonctionnalités Unix anciennes qui résout discrètement de vrais problèmes.
Modèle E : Docker rootless et user namespaces (sécurisé, mais ça change les règles)
Docker rootless et userns-remap sont d’excellents choix de sécurité. Ils réécrivent aussi le mappage UID/GID.
Votre UID 0 dans le conteneur peut être mappé à l’UID hôte 100000+.
Cela signifie qu’un répertoire hôte possédé par l’UID 0 n’est plus inscriptible par le « root » du conteneur — car ce n’est pas vraiment le root hôte.
Ce n’est pas un bug. C’est le but.
Blague n°2 : « Lancez-le en root » est l’équivalent conteneur de « redémarrez la prod ». Ça marche, et ça vous trahit aussi.
Tâches pratiques : commandes, sorties, décisions
Ci-dessous des tâches réelles que vous pouvez exécuter lors d’une réponse à incident ou d’un durcissement préventif.
Chacune inclut : la commande, ce que signifie la sortie, et quelle décision prendre.
Tâche 1 : Identifier l’UID/GID effectif du conteneur
cr0x@server:~$ docker exec -it myapp sh -lc 'id && umask'
uid=1001(app) gid=1001(app) groups=1001(app)
0022
Signification : le processus s’exécute en UID/GID 1001 et crée des fichiers avec umask par défaut 0022.
Décision : le répertoire monté doit être inscriptible par l’UID 1001 (propriétaire ou ACL) ou par un groupe auquel appartient le processus.
Tâche 2 : Inspecter quel utilisateur Docker pense qu’il exécute
cr0x@server:~$ docker inspect -f '{{.Config.User}}' myapp
1001:1001
Signification : le conteneur est configuré pour s’exécuter en user/group numériques.
Décision : alignez la propriété du répertoire hôte sur 1001:1001, ou changez l’utilisateur d’exécution pour correspondre au répertoire.
Tâche 3 : Confirmer la source du montage et son type
cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}' myapp
bind /srv/myapp/data -> /var/lib/myapp
volume myapp-cache -> /cache
Signification : /var/lib/myapp est un bind mount ; /cache est un volume nommé.
Décision : dépannez les permissions du bind mount sur le chemin hôte ; dépannez le volume nommé via l’emplacement des volumes Docker.
Tâche 4 : Vérifier la propriété et le mode du répertoire hôte
cr0x@server:~$ stat -c 'path=%n owner=%u:%g mode=%a type=%F' /srv/myapp/data
path=/srv/myapp/data owner=0:0 mode=755 type=directory
Signification : possédé par root, non inscriptible par les autres (755 signifie que seul le propriétaire peut écrire).
Décision : soit chown vers 1001:1001, soit exécuter le conteneur en root (non recommandé), soit utiliser ACL/écriture de groupe.
Tâche 5 : Simuler l’accès avec un conteneur jetable en tant qu’UID spécifique
cr0x@server:~$ docker run --rm -u 1001:1001 -v /srv/myapp/data:/mnt alpine sh -lc 'touch /mnt/test && ls -ln /mnt/test'
touch: /mnt/test: Permission denied
Signification : l’UID 1001 ne peut pas écrire dans le bind mount ; l’échec est reproductible en dehors de votre application.
Décision : corrigez d’abord les permissions du système de fichiers ; ne perdez pas de temps à « déboguer l’appli ».
Tâche 6 : Corriger la propriété du bind mount (ciblé, pas récursif sauf besoin)
cr0x@server:~$ sudo chown 1001:1001 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=755
Signification : le propriétaire du répertoire est maintenant UID/GID 1001. Mode 755 signifie que le propriétaire peut écrire.
Décision : retestez l’accès en écriture. Si l’appli a besoin d’une collaboration de groupe, ajustez le mode/ACL en conséquence.
Tâche 7 : Utiliser setgid + écriture de groupe pour des répertoires partagés
cr0x@server:~$ sudo chgrp 1001 /srv/myapp/data
cr0x@server:~$ sudo chmod 2775 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=2775
Signification : le bit setgid est défini (2xxx). Les nouveaux fichiers héritent du groupe 1001 ; le groupe a l’écriture.
Décision : utilisez ceci lorsque plusieurs processus partagent un GID et que vous voulez une propriété de groupe prévisible.
Tâche 8 : Ajouter une ACL pour un deuxième UID écrivain sans relâcher les bits de mode
cr0x@server:~$ sudo setfacl -m u:1002:rwx /srv/myapp/data
cr0x@server:~$ getfacl -p /srv/myapp/data | sed -n '1,12p'
# file: /srv/myapp/data
# owner: 1001
# group: 1001
user::rwx
user:1002:rwx
group::rwx
mask::rwx
other::r-x
Signification : l’UID 1002 a explicitement rwx sur le répertoire ; le mask le permet.
Décision : choisissez les ACL quand vous avez plusieurs UIDs à travers des conteneurs et que vous ne voulez pas que « tout le monde soit 1000 ».
Tâche 9 : Localiser un volume nommé sur l’hôte (pour débogage et initialisation)
cr0x@server:~$ docker volume inspect myapp-cache -f '{{.Mountpoint}}'
/var/lib/docker/volumes/myapp-cache/_data
Signification : le volume nommé se trouve sous la racine de données de Docker.
Décision : utilisez ce chemin pour l’inspection au niveau hôte (stat, getfacl) ou pour une configuration d’autorisation ponctuelle et prudente.
Tâche 10 : Inspecter la propriété du volume nommé et confirmer le décalage d’UID du conteneur
cr0x@server:~$ sudo stat -c 'owner=%u:%g mode=%a' /var/lib/docker/volumes/myapp-cache/_data
owner=0:0 mode=755
Signification : répertoire du volume possédé par root ; l’utilisateur conteneur 1001 ne peut pas écrire.
Décision : initialiser la propriété une fois (chown ciblé de ce dont vous avez besoin), puis exécuter l’appli sans privilèges.
Tâche 11 : Effectuer une initialisation one-time pour chown d’un volume en sécurité (portée limitée)
cr0x@server:~$ docker run --rm -v myapp-cache:/cache alpine sh -lc 'addgroup -g 1001 app && adduser -D -u 1001 -G app app; mkdir -p /cache/app; chown -R 1001:1001 /cache/app; ls -ldn /cache/app'
drwxr-xr-x 2 1001 1001 4096 Feb 4 12:00 /cache/app
Signification : créé un sous-répertoire /cache/app possédé par 1001:1001, sans chowner récursivement toute la racine du volume.
Décision : montez et utilisez ce sous-répertoire depuis l’appli pour éviter les conflits de propriété inattendus.
Tâche 12 : Valider dans le conteneur applicatif que l’écriture fonctionne maintenant
cr0x@server:~$ docker exec -it myapp sh -lc 'touch /cache/app/ok && ls -ln /cache/app/ok'
-rw-r--r-- 1 1001 1001 0 Feb 4 12:01 /cache/app/ok
Signification : fichier créé avec la propriété numérique correcte.
Décision : vous avez terminé — sauf si SELinux/AppArmor ou des sémantiques NFS sont impliqués.
Tâche 13 : Vérifier les montages en lecture seule (surprenamment courant)
cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{if .RW}}{{else}}{{println "RO:" .Destination}}{{end}}{{end}}' myapp
RO: /var/lib/myapp
Signification : le montage est en lecture seule au niveau Docker.
Décision : corrigez d’abord les flags dans Compose/run ; les permissions n’auront pas d’importance si le montage est RO.
Tâche 14 : Vérification SELinux (bind mounts qui devraient marcher mais ne marchent pas)
cr0x@server:~$ getenforce
Enforcing
Signification : SELinux applique la politique.
Décision : si les bind mounts échouent malgré un UID/GID/mode corrects, labellisez le bind mount de manière appropriée (par exemple avec :Z/:z) et retestez.
Tâche 15 : Détecter le comportement NFS root-squash (root ne peut pas réparer ce que root ne peut pas posséder)
cr0x@server:~$ mount | grep ' /srv/myapp '
nfs01:/exports/myapp on /srv/myapp type nfs4 (rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.10,local_lock=none,addr=10.0.0.20)
Signification : le stockage sous-jacent est NFSv4. Root-squash est configuré côté serveur, non visible ici.
Décision : si chown échoue avec « Operation not permitted », arrêtez de vous battre avec le conteneur et corrigez le mappage d’identité/options d’export.
Tâche 16 : Vérifier le remappage des user namespaces (les UIDs ne correspondront pas comme vous le pensez)
cr0x@server:~$ docker info | sed -n '1,120p' | grep -E 'rootless|userns'
rootless: false
userns: host
Signification : pas de userns remap ; les UIDs du conteneur se mappent directement aux UIDs hôte.
Décision : l’alignement UID/GID est simple. Si userns est activé, intégrez les plages de remappage dans votre plan.
Tâche 17 : Confirmer que le kernel voit le montage là où vous pensez qu’il est
cr0x@server:~$ docker exec -it myapp sh -lc 'grep -E " /var/lib/myapp | /cache " /proc/mounts'
/dev/sda1 /var/lib/myapp ext4 rw,relatime 0 0
/dev/sda1 /cache ext4 rw,relatime 0 0
Signification : les montages sont présents et rw au niveau noyau.
Décision : si les écritures échouent toujours, c’est les permissions, SELinux/AppArmor, attributs immuables, ou les sémantiques du système de fichiers en réseau.
Tâche 18 : Vérifier l’attribut immuable (rare, mais ça arrive)
cr0x@server:~$ sudo lsattr -d /srv/myapp/data
-------------------P-- /srv/myapp/data
Signification : pas d’attribut immuable (i) défini.
Décision : si vous voyez ----i--------, retirez-le avec chattr -i (avec précaution) avant de chasser d’autres fantômes.
Erreurs courantes : symptômes → cause racine → correction
1) Symptom: « Permission denied » au démarrage, mais seulement sur un hôte
Cause racine : le bind mount pointe vers un répertoire avec une propriété/ACL différente sur cet hôte (souvent créé manuellement ou par une exécution d’automatisation différente).
Correction : standardisez la création de répertoires dans le provisioning. Faites respecter la propriété/mode via la gestion de configuration. Exécutez les conteneurs avec des UID/GID numériques explicites.
2) Symptom: le conteneur s’exécute en root, ne peut toujours pas écrire
Cause racine : mismatch d’étiquette SELinux, montage en lecture seule, ou NFS root-squash.
Correction : vérifiez le flag RW du montage ; vérifiez getenforce ; si NFS, corrigez le mapping d’identité de l’export ; si SELinux, labellisez le montage correctement.
3) Symptom: « Operation not permitted » lors d’un chown d’un répertoire monté
Cause racine : le système de fichiers ne supporte pas chown comme attendu (commun avec NFS/CIFS), ou vous n’êtes pas réellement privilégié pour changer la propriété là.
Correction : alignez les identités au niveau du stockage (mappage UID), ou utilisez des ACL supportées par ce système de fichiers, ou écrivez dans un répertoire déjà possédé correctement.
4) Symptom: ça marche après suppression du volume, puis ça casse à nouveau
Cause racine : vous avez « réparé » en réinitialisant l’état. La prochaine exécution recrée les répertoires avec root comme propriétaire ou un UID différent.
Correction : ajoutez une étape d’initialisation déterministe (créer un sous-répertoire + chown une fois) et cessez de compter sur des valeurs par défaut implicites.
5) Symptom: fichiers créés par l’appli sont possédés par root sur l’hôte
Cause racine : le processus conteneur tourne en root, ou l’entrypoint de l’image change d’utilisateur incorrectement, ou vous avez utilisé user: "0" comme correctif rapide.
Correction : exécutez en UID numérique non-root ; assurez-vous que l’entrypoint ne ré-élève pas les privilèges ; vérifiez avec id dans le conteneur.
6) Symptom: le sidecar d’envoi de logs/sauvegarde ne peut pas lire les fichiers créés par l’appli
Cause racine : umask trop strict, pas de stratégie de groupe partagé, ou pas d’ACL pour le second UID.
Correction : définissez la propriété de groupe + setgid sur les répertoires ; ou ajoutez une ACL ; ou alignez les deux conteneurs sur un GID partagé.
7) Symptom: après activation de Docker rootless, tout est « Permission denied »
Cause racine : les chemins hôtes sont possédés par le vrai root, mais le « root » du conteneur rootless est mappé sur une plage d’UID hôte non privilégiée.
Correction : chown des répertoires hôtes vers l’utilisateur rootless (ou ajustez les mappings subuid/subgid), ou évitez les bind mounts nécessitant la propriété root de l’hôte.
8) Symptom: délai de démarrage massif lorsque le conteneur démarre
Cause racine : l’entrypoint fait un chown récursif sur un grand dataset monté.
Correction : retirez le chown récursif du chemin chaud ; faites une initialisation one-time ; ciblez un sous-répertoire ; ou utilisez le provisioning au niveau du système de fichiers.
9) Symptom: les permissions semblent correctes, mais les écritures échouent seulement en production
Cause racine : la production utilise SELinux en mode enforcing ou un backend de stockage différent (NFS/CephFS) avec des sémantiques différentes.
Correction : testez avec des paramètres de sécurité similaires à la production ; gérez explicitement les labels SELinux et le mapping d’identité des systèmes de fichiers réseau.
Trois mini-histoires d’entreprise issues du terrain
Mini-histoire n°1 : L’incident causé par une mauvaise hypothèse
Une équipe a déployé un job runner containerisé qui écrivait des artefacts dans un répertoire bind-monté sous /srv/builds.
En staging, tout allait bien. En production, les artefacts échouaient avec Permission denied de façon aléatoire.
L’astreinte a passé en revue les suspects habituels : panne de stockage, disque plein, image cassée.
La mauvaise hypothèse était subtile : « le répertoire hôte est toujours créé par notre automatisation, donc il appartient toujours au compte de service. »
Sauf qu’un hôte de production avait été reconstruit à la hâte pendant une fenêtre de maintenance. Un humain a recréé /srv/builds manuellement.
Root le possédait, mode 755, et le conteneur tournait en UID 1001.
Le pattern d’échec semblait aléatoire parce que les jobs étaient répartis sur plusieurs hôtes. Ceux tombant sur l’hôte « fait à la main » échouaient.
Personne n’a remarqué la différence de propriété avant que quelqu’un n’exécute stat sur la flotte et ne voie l’élément aberrant.
La correction n’a rien d’héroïque. Ils ont ajouté une simple vérification pré-vol dans le provisioning qui assert la propriété et le mode, et ont configuré Compose pour exécuter le conteneur en 1001:1001 explicitement.
Surtout, ils ont arrêté de compter sur les noms d’utilisateur dans la documentation — chaque runbook a commencé à utiliser des UID/GID numériques.
La leçon : si l’existence d’un répertoire fait partie de la correctude, alors sa propriété en fait aussi partie. Linux n’interprète pas les bonnes intentions.
Mini-histoire n°2 : L’optimisation qui a mal tourné
Une autre organisation avait un service de traitement de gros volumes. Pour réduire la charge opérationnelle, ils ont ajouté une étape de démarrage qui faisait
chown -R app:app /data à chaque boot de conteneur. Les tickets ont disparu — brièvement.
Cela a aussi rendu les déploiements « fiables », dans le sens où attendre deux heures est fiablement lent.
Au début, ce n’était qu’une nuisance. Puis le dataset a grossi. Une mise à jour progressive signifiait plusieurs conteneurs en parallèle, chacun parcourant récursivement un arbre de téraoctets.
Le CPU a flambé, les I/O métadonnées ont explosé, et le backend de stockage a commencé à générer des alertes de latence. L’appli était « healthy », pas le cluster.
Le revers n’était pas que de la performance. Le chown récursif a modifié les timestamps et la propriété de fichiers utilisés par des systèmes en aval pour détecter la « fraîcheur ».
Plusieurs pipelines ont retraité des données parce que la propriété a changé, ce qui a changé les métadonnées, ce qui a déclenché leurs heuristiques de « nouvelles données ».
Personne n’était amusé.
La correction a été de sortir l’initialisation des permissions du conteneur runtime :
un job de provisioning one-time a créé un sous-répertoire dédié et a défini la propriété une fois.
Ils ont aussi modifié l’application pour qu’elle refuse de démarrer si elle ne peut pas écrire, au lieu d’essayer de « réparer » les permissions. Échouer vite, échouer honnêtement.
La leçon : les « permissions auto-réparantes » sont souvent juste « brûler lentement votre système de fichiers ».
Mini-histoire n°3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une équipe plateforme a standardisé une politique : chaque conteneur stateful doit déclarer un user numérique dans Compose/Kubernetes,
et chaque montage inscriptible doit être provisionné avec la propriété correspondante avant que la charge de travail ne soit planifiée.
Pas d’exceptions, pas de « root temporaire », pas de 777. Cela a agacé les développeurs pendant environ une semaine.
Des mois plus tard, ils ont migré une flotte de services des disques locaux vers un stockage NFS pour simplifier les sauvegardes.
NFS a introduit ses bizarreries de mapping d’identité. Prédictiblement, quelques services ont commencé à lancer des erreurs de permission lors du premier canari.
Mais comme l’UID/GID était déclaré et cohérent, la surface de dépannage était minime : c’était soit le mapping d’export soit le provisioning du répertoire.
L’astreinte n’a pas deviné. Ils ont vérifié l’UID déclaré, vérifié la propriété du répertoire sur le serveur NFS, corrigé le mapping, et ont poursuivi.
Pas de rebuild d’image. Pas d’« exécuter en root » d’urgence. Pas de chmod à minuit.
La pratique était ennuyeuse : identités numériques explicites, chemins inscriptibles pré-provisionnés, et une vérification pré-vol en CI validant que le conteneur tourne non-root.
Cette monotonie a payé.
La leçon : la standardisation bat l’ingéniosité, surtout autour des permissions.
Faits intéressants & contexte historique
- Les UIDs et GIDs précèdent les conteneurs de plusieurs décennies. Docker n’a pas créé le modèle ; il repose sur la propriété Unix classique et les bits de mode.
- Les noms sont décoratifs. Linux stocke la propriété sous forme d’entiers dans les inodes ; les noms d’utilisateur sont résolus plus tard via
/etc/passwdou NSS. - Les premières configurations de conteneurs étaient par défaut en root. C’était pratique, et cela a habitué une génération à normaliser l’exécution des services en UID 0.
- Les user namespaces ont changé la définition de « root ». Avec userns ou le mode rootless, le root du conteneur peut se mapper à une plage d’UID hôte non privilégiée.
- Les systèmes de fichiers en overlay n’« ont pas réparé » les permissions. OverlayFS résout le layering ; les volumes montés respectent toujours la propriété et la politique du système de fichiers sous-jacent.
- Le NFS root-squash est plus ancien que la plupart des plateformes conteneur. C’est une fonctionnalité de sécurité côté stockage qui rend le root du client similaire à « nobody ».
- Les ACL ne sont pas nouvelles. Les ACL POSIX existent depuis des années ; elles sont simplement sous-utilisées car les bits de mode sont plus faciles à expliquer.
- Le labelling SELinux est une autre dimension que UID/GID. Vous pouvez avoir une propriété parfaite et être quand même refusé par la politique MAC.
- Beaucoup d’images officielles standardisent des UIDs fixes. Les images de bases de données utilisent souvent des IDs numériques stables pour que les volumes restent compatibles lors des upgrades — si vous respectez ces IDs.
Listes de contrôle / plan étape par étape
Plan étape par étape : faire en sorte qu’un conteneur stateful cesse de se battre avec son volume
-
Choisir l’identité d’exécution.
Décidez de l’UID/GID numérique que le service doit utiliser à l’exécution. Utilisez un numéro dédié et stable (pas « ce que l’image de base a choisi aujourd’hui »). -
Choisir une stratégie de montage.
Bind mount pour un contrôle explicite de l’hôte ; volume nommé pour la portabilité ; stockage réseau seulement si vous comprenez son modèle d’identité. -
Provisionner le chemin inscriptible.
Créez le répertoire (ou un sous-répertoire) et définissez la propriété/mode. Préférez un chown ciblé plutôt que récursif. -
Rendre testable.
Ajoutez une vérification pré-vol : le conteneur peut-il écrire un fichier temporaire sur le montage au démarrage ? Sinon, échouez vite avec une ligne de log claire. -
Séparer l’init du runtime.
Si vous devez chown, faites-le comme job d’initialisation one-time ou comme étape opérationnelle manuelle, pas à chaque démarrage du conteneur. -
Gérer SELinux/AppArmor explicitement.
Si vous exécutez SELinux en enforcing, intégrez le labelling des montages dans votre configuration run/compose. -
Documenter les IDs numériques.
Mettez l’UID/GID dans le repo à côté des manifests Compose. Les noms dérivent ; les numéros non. -
Répéter le plan de reprise après sinistre.
Si vous restaurez un volume depuis une sauvegarde, conserve-t-il la propriété ? Sinon, quelle étape de ré-propriété est requise ?
Checklist pré-déploiement (à utiliser avant la première exécution en production)
- Le conteneur s’exécute en non-root (vérifier avec
iddans le conteneur). - Les destinations de montage sont inscriptibles par cet UID/GID (vérifier avec un test
touchjetable). - Pas de chown récursif dans l’entrypoint pour de gros chemins de données.
- Les systèmes SELinux enforcing ont une stratégie de labellisation correcte pour les bind mounts.
- Les systèmes de fichiers réseau ont un plan de mapping d’identité (parité UID ou répertoire possédé correctement).
- Le processus de sauvegarde/restauration préserve ou ré-applique la propriété de manière déterministe.
Checklist d’incident (quand « Permission denied » vous réveille)
- Confirmer l’UID/GID du conteneur (
docker exec ... id). - Confirmer le type de montage et le statut RW (
docker inspect). - Vérifier la propriété/mode/ACL du chemin hôte (
stat,getfacl). - Vérifier SELinux enforcing (
getenforce) et les logs d’audit si applicable. - Contrôler NFS/CIFS et les symptômes de root-squash (
mount, comportement de chown). - Appliquer la plus petite correction sûre : alignement de propriété, ACL ou labelling correct — pas 777.
FAQ
1) Pourquoi mon utilisateur de conteneur existe dans l’image mais pas sur l’hôte ?
Parce que ce sont des namespaces séparés pour les noms d’utilisateur. Seuls les IDs numériques comptent pour les vérifications de permission du système de fichiers.
L’hôte n’a pas besoin de connaître le nom d’utilisateur ; il faut que l’UID/GID corresponde à la propriété ou aux permissions accordées.
2) Si j’exécute le conteneur avec -u 1000:1000, ai-je besoin que cet utilisateur existe dans l’image ?
Pas strictement. Le noyau applique des IDs numériques. Certaines applications attendent une entrée passwd pour l’UID (pour les appels getpwuid()).
Si l’appli se plaint, ajoutez une entrée ou utilisez une image qui supporte des UIDs arbitraires.
3) Est-ce sûr de chown un répertoire de volume Docker sous /var/lib/docker ?
Cela peut l’être, mais soyez discipliné. Préférez créer et posséder un sous-répertoire à l’intérieur du volume plutôt que de changer la racine du volume.
Évitez le chown récursif sur de grandes données. Et ne lancez jamais de commandes « devinant » en root dans cet arbre pendant un incident.
4) Pourquoi les permissions cassent après une restauration depuis la sauvegarde ?
Beaucoup d’outils de sauvegarde préservent le contenu mais pas la propriété numérique, ou ils restaurent en tant qu’utilisateur qui exécute la restauration.
Si les fichiers restaurés appartiennent à root (ou à un UID différent), votre utilisateur conteneur perdra l’accès en écriture.
Corrigez en restaurant avec la propriété préservée ou en exécutant une étape contrôlée de ré-propriété ensuite.
5) Quelle est la différence entre chmod et chown dans ce contexte ?
chown change qui possède les fichiers (quel UID/GID obtient les droits de propriétaire).
chmod change ce que les propriétaires/groupes/autres peuvent faire. Si le propriétaire est mauvais, chmod réarrange souvent juste la déception.
6) Mon appli s’exécute en root dans le conteneur. Ce n’est pas grave parce que c’est « isolé » ?
L’isolation n’est pas absolue. Root dans le conteneur a de larges pouvoirs dans ce contexte conteneur, et des mauvaises configurations arrivent.
S’exécuter en non-root réduit le rayon d’impact, et vous force à résoudre correctement les permissions des volumes au lieu de les masquer.
7) Pourquoi ça marche sur Docker Desktop macOS/Windows mais échoue sur Linux ?
Docker Desktop utilise une VM et un mécanisme de partage de système de fichiers différent. Les sémantiques de propriété et de permission peuvent être traduites ou simplifiées.
Les hôtes Linux sont « réels » dans le sens où le bind mount est le système de fichiers effectif avec de vrais UIDs, ACLs et modules de sécurité.
8) Comment gérer plusieurs conteneurs écrivant sur le même volume ?
Privilégiez une stratégie de GID partagé (écriture de groupe + setgid) ou utilisez des ACL pour accorder l’écriture à plusieurs UIDs.
Évitez d’exécuter tout le monde sous le même UID sauf si vous êtes prêt pour les conséquences en audit et en débogage.
9) Et si j’utilise Kubernetes au lieu de Docker Compose ?
Les principes sont les mêmes : alignez l’UID/GID d’exécution avec les permissions du stockage. Kubernetes ajoute des options securityContext comme
runAsUser et fsGroup. Attention : fsGroup peut déclencher des changements de permission récursifs sur certains types de volumes,
ce qui est bien pour de petits volumes et pénible pour de gros.
10) Est-ce que umask fait partie de l’histoire ?
Oui. Même si la propriété du répertoire est correcte, un umask restrictif peut créer des fichiers que d’autres processus ne peuvent pas lire.
Quand vous avez des sidecars ou des lecteurs partagés, décidez intentionnellement si la lecture/écriture de groupe est requise et définissez l’umask en conséquence.
Conclusion : prochaines étapes durables
« Permission denied » sur les volumes Docker est rarement mystérieux. C’est presque toujours un UID/GID numérique décalé, un manque de permission de groupe/ACL,
ou une couche de sécurité (SELinux, NFS root-squash) qui fait son travail.
Le correctif durable est d’arrêter d’improviser et de commencer à déclarer l’identité et la propriété comme partie du contrat de déploiement.
Faites ceci ensuite (dans l’ordre)
- Choisissez un UID/GID numérique stable pour chaque service stateful et notez-le dans le repo.
- Provisionnez volontairement les répertoires inscriptibles (propriétaire/mode, ou ACLs, ou groupe+setgid), pas par accident.
- Exécutez les conteneurs en tant que cet UID/GID et vérifiez avec un simple test d’écriture au démarrage ou en CI.
- Retirez le chown récursif des entrypoints sauf si votre chemin de données est minuscule et que vous aimez les déploiements lents.
- Tenez compte de SELinux et du stockage réseau tôt ; ces problèmes ne deviennent pas plus simples à 2h du matin.
Si vous traitez les volumes comme partie de l’application — pas comme un après-coup — vous cesserez de jouer au whack-a-mole des permissions.
Votre futur vous recevra encore des pages, bien sûr. Juste pour quelque chose de plus intéressant.