Corriger les erreurs « Permission denied » sur les volumes Docker (Linux & macOS)
Les erreurs de type « Permission denied » avec Docker apparaissent presque toujours quand un processus dans le conteneur tente de lire/écrire sur un chemin qui est en réalité un volume (bind mount ou volume nommé) dont les permissions/UID/GID ne correspondent pas à l’utilisateur effectif du processus. Le problème est fréquent avec des images qui tournent en non-root (bonnes pratiques), avec des frameworks qui écrivent des caches/logs, ou avec des montages de répertoires du host vers le conteneur.
Ce tutoriel explique en profondeur comment diagnostiquer et corriger ces erreurs sur Linux et macOS, avec des commandes réelles, des stratégies robustes, et des pièges courants.
Sommaire
- 1. Comprendre le modèle de permissions avec Docker
- 2. Identifier le type de volume : bind mount vs volume nommé
- 3. Diagnostiquer : quel utilisateur écrit ? quel chemin ?
- 4. Cas Linux : corriger proprement
- 4.1. Solution A : aligner UID/GID du conteneur sur le host
- 4.2. Solution B :
chowndu répertoire monté (host) - 4.3. Solution C :
chownau démarrage du conteneur (entrypoint) - 4.4. Solution D : ACL (
setfacl) pour des droits fins - 4.5. SELinux (Fedora/RHEL/CentOS) : labels
:Z/:z - 4.6. Rootless Docker : subtilités et limites
- 5. Cas macOS : spécificités Docker Desktop
- 6. Exemples concrets (Node, Python, PostgreSQL, Nginx)
- 7. Check-list rapide de résolution
- 8. Bonnes pratiques pour éviter le problème
1. Comprendre le modèle de permissions avec Docker
1.1. UID/GID : la vraie identité, pas le nom
Sous Linux (et dans la VM Linux de Docker Desktop), les permissions reposent sur :
- UID (User ID)
- GID (Group ID)
- bits de permission (rwx) et éventuellement ACL/SELinux
Dans un conteneur, un utilisateur peut s’appeler app, node, www-data, etc. Mais ce qui compte réellement pour le kernel, c’est son UID/GID.
Exemple : www-data est souvent UID 33. Si votre répertoire monté sur le host appartient à UID 1000, et que le conteneur écrit en UID 33, vous pouvez obtenir Permission denied.
1.2. Bind mount vs volume nommé : différence de comportement
- Bind mount : vous montez un chemin du host (ex:
./data:/var/lib/postgresql/data). Les permissions visibles dans le conteneur reflètent celles du host (avec quelques nuances sur macOS). - Volume nommé : Docker gère un répertoire dans son espace (ex:
pgdata:/var/lib/postgresql/data). Les permissions sont gérées dans l’environnement Docker (souvent plus simple).
En pratique, les erreurs de permission sont plus fréquentes avec les bind mounts.
2. Identifier le type de volume : bind mount vs volume nommé
2.1. Inspecter un conteneur
docker inspect <container_name_or_id> --format '{{json .Mounts}}' | jq
Vous verrez des entrées du type :
Type: "bind"avecSource: "/chemin/host"Type: "volume"avecName: "monvolume"
Sans jq, vous pouvez faire :
docker inspect <container> | sed -n '/"Mounts": \[/,/\],/p'
2.2. Inspecter un volume nommé
docker volume inspect monvolume
Vous obtiendrez notamment Mountpoint (chemin interne au moteur Docker).
3. Diagnostiquer : quel utilisateur écrit ? quel chemin ?
3.1. Reproduire l’erreur et lire le message exact
Exemples typiques :
EACCES: permission denied, mkdir '/app/node_modules'Permission denied: '/var/log/nginx/access.log'chown: changing ownership of ...: Operation not permittedinitdb: error: could not create directory ... Permission denied
Le message vous donne souvent :
- le chemin
- l’opération (mkdir, open, chown, unlink…)
3.2. Vérifier l’utilisateur effectif dans le conteneur
docker exec -it <container> sh -lc 'id && whoami && umask'
Regardez uid=... gid=....
3.3. Vérifier les permissions sur le chemin dans le conteneur
docker exec -it <container> sh -lc 'ls -ld /chemin && ls -l /chemin | head'
Et si possible, tentez une écriture :
docker exec -it <container> sh -lc 'touch /chemin/test && echo ok'
Si touch échoue, vous êtes bien face à un problème de droits (ou SELinux / partage macOS).
3.4. Vérifier le propriétaire côté host (Linux)
Sur Linux, si c’est un bind mount, vérifiez :
ls -ld ./data
stat -c 'owner=%U uid=%u group=%G gid=%g perms=%A' ./data
4. Cas Linux : corriger proprement
4.1. Solution A : aligner UID/GID du conteneur sur le host
C’est souvent la solution la plus propre pour les environnements de dev : faire tourner le processus dans le conteneur avec le même UID/GID que votre utilisateur Linux (souvent 1000:1000).
4.1.1. Récupérer UID/GID sur le host
id -u
id -g
4.1.2. Avec docker run
docker run --rm -it \
-u "$(id -u):$(id -g)" \
-v "$PWD:/app" \
-w /app \
node:20-alpine sh
Si votre application écrit dans /app, elle écrira avec votre UID/GID.
4.1.3. Avec Docker Compose
Dans compose.yaml (exemple) :
# (commande affichée ici pour contexte, à mettre dans votre fichier compose)
Paramètre important : user: "1000:1000" ou user: "${UID}:${GID}".
Vous pouvez exporter des variables :
export UID GID
UID=$(id -u) GID=$(id -g) docker compose up
Ou créer un .env :
printf "UID=%s\nGID=%s\n" "$(id -u)" "$(id -g)" > .env
docker compose up
Pourquoi ça marche ?
Parce que le kernel voit les mêmes identifiants. Le conteneur n’est pas « magique » : un bind mount reste un répertoire du host, donc les permissions du host s’appliquent.
Limites :
- Certaines images supposent un utilisateur interne spécifique (ex:
postgres) et peuvent mal fonctionner si vous changez l’utilisateur. - Certains répertoires dans l’image (hors volume) peuvent ne plus être accessibles si l’utilisateur n’a pas les droits.
4.2. Solution B : chown du répertoire monté (host)
Si vous voulez garder l’utilisateur interne de l’image (ex: postgres), vous pouvez ajuster le propriétaire du répertoire sur le host pour qu’il corresponde à l’UID/GID attendu dans le conteneur.
4.2.1. Trouver l’UID/GID attendu dans l’image
Exemple PostgreSQL :
docker run --rm postgres:16 sh -lc 'id postgres'
Vous obtenez typiquement uid=999 gid=999.
4.2.2. Appliquer chown sur le host
Si vous montez ./pgdata vers /var/lib/postgresql/data :
sudo mkdir -p ./pgdata
sudo chown -R 999:999 ./pgdata
sudo chmod -R u+rwX,go-rwx ./pgdata
Puis lancez :
docker run --rm \
-v "$PWD/pgdata:/var/lib/postgresql/data" \
-e POSTGRES_PASSWORD=secret \
postgres:16
Attention :
chownsur un répertoire de projet peut être gênant (vos fichiers deviennent « appartenant » à un UID inconnu localement).- Préférez cette méthode pour des répertoires dédiés (ex:
./.docker/pgdata).
4.3. Solution C : chown au démarrage du conteneur (entrypoint)
Approche courante : démarrer en root, corriger les permissions, puis lancer l’app en non-root.
4.3.1. Exemple d’entrypoint shell
docker-entrypoint.sh :
#!/bin/sh
set -eu
# Exemple: rendre /data accessible à l'utilisateur app (UID 10001)
chown -R 10001:10001 /data
exec su-exec 10001:10001 "$@"
Dans Alpine, su-exec est souvent préféré à gosu :
apk add --no-cache su-exec
4.3.2. Exemple Dockerfile
docker build -t monapp .
Contenu (extrait) :
# Exemple basé sur alpine
FROM alpine:3.20
RUN addgroup -g 10001 app && adduser -D -u 10001 -G app app \
&& apk add --no-cache su-exec
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
USER root
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["sh", "-lc", "id && touch /data/ok && sleep 3600"]
Run :
mkdir -p data
docker run --rm -v "$PWD/data:/data" monapp
Avantages :
- Automatique, reproductible.
- Permet de garder l’app en non-root.
Inconvénients :
chown -Rpeut être coûteux sur de gros volumes.- Sur certains systèmes/partages,
chownpeut être interdit (Operation not permitted).
4.4. Solution D : ACL (setfacl) pour des droits fins
Si vous ne voulez pas changer le propriétaire, vous pouvez donner des droits à un UID/GID spécifique via ACL.
Installer les outils si besoin :
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y acl
# Fedora
sudo dnf install -y acl
Donner à l’UID 33 (www-data) le droit d’écrire dans ./var :
sudo setfacl -R -m u:33:rwx ./var
sudo setfacl -R -d -m u:33:rwx ./var
-mmodifie l’ACL existante-drègle l’ACL par défaut (héritée par les nouveaux fichiers)
Vérifier :
getfacl ./var | sed -n '1,80p'
Quand utiliser ACL ?
- Quand plusieurs utilisateurs/containers doivent écrire dans le même répertoire.
- Quand vous voulez éviter de
chownle projet.
4.5. SELinux (Fedora/RHEL/CentOS) : labels :Z / :z
Sur des distributions avec SELinux en mode enforcing, vous pouvez avoir Permission denied même si les bits Unix semblent corrects.
4.5.1. Symptôme
- Tout semble OK (
ls -l), mais l’écriture échoue. - Les logs SELinux (audit) montrent un refus.
Vérifier l’état :
getenforce
4.5.2. Corriger avec :Z ou :z sur le bind mount
:Z: label privé (unique) pour ce conteneur:z: label partagé (plusieurs conteneurs)
Exemple :
docker run --rm -it \
-v "$PWD/data:/data:Z" \
alpine sh -lc 'touch /data/test && ls -l /data'
Avec Compose, sur la ligne du volume :
./data:/data:Z
Pourquoi ?
SELinux contrôle l’accès en plus des permissions Unix. Le label de sécurité du répertoire doit autoriser le domaine du conteneur.
4.6. Rootless Docker : subtilités et limites
En mode rootless, Docker tourne sans privilèges root sur le host. Cela améliore la sécurité mais peut introduire des contraintes :
- certains
chown/chmodsur bind mounts peuvent échouer - les UID/GID sont mappés via user namespaces
Vérifier si vous êtes en rootless :
docker info | sed -n '/Rootless/p'
Stratégies :
- privilégier les volumes nommés pour les données
- éviter les opérations
chowncoûteuses ou impossibles sur bind mounts - aligner
user:dans Compose avec votre UID/GID
5. Cas macOS : spécificités Docker Desktop
Sur macOS, Docker Desktop exécute les conteneurs dans une VM Linux. Les bind mounts passent par un mécanisme de partage de fichiers (virtiofs / gRPC FUSE selon versions). Résultat : les permissions peuvent se comporter différemment qu’un Linux natif.
5.1. Partage de fichiers et droits « surprenants »
Sur macOS, vous pouvez voir :
- des fichiers appartenant à
rootdans le conteneur - des
chmod/chownqui semblent réussir ou échouer de manière inattendue - des performances moindres sur de gros arbres de fichiers
La règle pratique : sur macOS, évitez de compter sur chown sur un bind mount comme solution universelle. Préférez :
- exécuter le conteneur avec un utilisateur compatible (
-u) - ou utiliser des volumes nommés pour les répertoires qui nécessitent des écritures intensives (caches, node_modules, etc.)
5.2. Autoriser l’accès aux dossiers dans Docker Desktop
Si Docker n’a pas accès au chemin, vous pouvez obtenir des erreurs proches de permission.
Vérifiez dans Docker Desktop :
- Settings → Resources → File Sharing
- Ajoutez le dossier parent de votre projet (ex:
/Users/vous/Projects)
Ensuite redémarrez Docker Desktop.
Test rapide :
docker run --rm -v "$PWD:/app" alpine sh -lc 'ls -la /app | head'
Si /app est vide ou inaccessible, c’est souvent un problème de partage.
5.3. Éviter les bind mounts pour les caches : volumes nommés
Exemple Node : monter le code en bind mount, mais garder node_modules dans un volume nommé (géré côté Linux VM), ce qui réduit les problèmes de droits et améliore souvent les performances.
docker run --rm -it \
-v "$PWD:/app" \
-v node_modules:/app/node_modules \
-w /app \
node:20-alpine sh -lc 'npm ci && npm test'
Créer/inspecter :
docker volume ls
docker volume inspect node_modules
6. Exemples concrets (Node, Python, PostgreSQL, Nginx)
6.1. Node.js : EACCES sur node_modules ou .npm
Symptôme
npm ERR! code EACCESpermission denied, mkdir '/app/node_modules'EACCES: permission denied, open '/home/node/.npm/_cacache/...
Approche recommandée (dev)
- bind mount du code
- volume nommé pour
node_modules - éventuellement exécuter en UID/GID host
Exemple :
docker run --rm -it \
-u "$(id -u):$(id -g)" \
-v "$PWD:/app" \
-v node_modules:/app/node_modules \
-w /app \
node:20-alpine sh -lc 'node -v && npm -v && npm ci'
Si l’image attend l’utilisateur node (UID 1000 dans certaines images), vous pouvez aussi :
docker run --rm -it \
-v "$PWD:/app" \
-v node_modules:/app/node_modules \
-w /app \
node:20-alpine sh -lc 'id && npm ci'
Si ça échoue, revenez à -u "$(id -u):$(id -g)".
6.2. Python : erreurs d’écriture dans /app ou dans un venv
Symptôme
PermissionError: [Errno 13] Permission denied: ...- impossibilité de créer
.pytest_cache,__pycache__,.mypy_cache
Solution simple : aligner l’utilisateur :
docker run --rm -it \
-u "$(id -u):$(id -g)" \
-v "$PWD:/app" \
-w /app \
python:3.12-slim bash -lc 'python -c "import os; print(os.getuid())" && pytest -q'
Alternative : déplacer caches vers /tmp (souvent writable) via variables d’environnement selon outils.
6.3. PostgreSQL : initdb ne peut pas écrire dans le data dir
Symptôme
initdb: error: could not create directory "/var/lib/postgresql/data": Permission denied
Si vous utilisez un bind mount ./pgdata:/var/lib/postgresql/data, assurez-vous que ./pgdata appartient à l’UID postgres (souvent 999).
docker run --rm postgres:16 sh -lc 'id postgres'
sudo mkdir -p ./pgdata
sudo chown -R 999:999 ./pgdata
Puis :
docker run --rm \
-v "$PWD/pgdata:/var/lib/postgresql/data" \
-e POSTGRES_PASSWORD=secret \
postgres:16
Sur macOS, préférez un volume nommé :
docker run --rm \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
6.4. Nginx : logs en écriture interdite
Symptôme
open() "/var/log/nginx/access.log" failed (13: Permission denied)
Dans certaines images, nginx tourne en nginx ou www-data. Si vous montez /var/log/nginx depuis le host, il faut que ce répertoire soit writable par l’utilisateur du process.
Diagnostic :
docker exec -it <nginx> sh -lc 'id && ps aux | grep [n]ginx'
docker exec -it <nginx> sh -lc 'ls -ld /var/log/nginx'
Correction (Linux bind mount) :
- trouver l’UID :
docker run --rm nginx:alpine sh -lc 'id nginx || id www-data'
- appliquer
chownsur le host (ex: UID 101) :
sudo chown -R 101:101 ./nginx-logs
Alternative : ne pas bind-mounter les logs, et laisser Nginx écrire dans le conteneur (ou rediriger vers stdout/stderr, pratique recommandée en conteneurs).
7. Check-list rapide de résolution
- Quel chemin provoque l’erreur ? (message d’erreur exact)
- Est-ce un bind mount ou un volume nommé ?
- bind mount → permissions host cruciales
- volume nommé → permissions gérées côté Docker
- Dans le conteneur :
id(UID/GID)ls -ld <chemin>- test
touch <chemin>/test
- Sur Linux host (si bind mount) :
statdu répertoire- ajuster via
chown/chmod/ ACL
- Sur Fedora/RHEL/CentOS :
- tester
:Zsur le mount (SELinux)
- tester
- Sur macOS :
- vérifier Docker Desktop File Sharing
- préférer volumes nommés pour caches/données
8. Bonnes pratiques pour éviter le problème
8.1. Séparer « code » et « données/caches »
- Code source : bind mount (pratique en dev)
- Données persistantes, caches,
node_modules, DB data : volumes nommés
Exemple pattern :
docker run --rm -it \
-v "$PWD:/app" \
-v app_cache:/app/.cache \
-w /app \
-u "$(id -u):$(id -g)" \
monimage
8.2. Éviter d’écrire dans des répertoires système
Évitez d’écrire dans / ou /usr etc. Utilisez :
/tmp/var/lib/<app>/home/<user>- un répertoire dédié monté
8.3. Préférer les images qui supportent l’exécution non-root
Beaucoup d’images officielles ont des mécanismes pour gérer les permissions (ex: postgres, nginx, images Bitnami, etc.). Lisez la doc de l’image : certaines attendent un répertoire avec des permissions précises.
8.4. Documenter l’UID/GID attendu
Dans vos projets, notez :
- l’utilisateur du conteneur
- l’UID/GID
- les répertoires qui doivent être writable
Commande utile pour inspection rapide :
docker run --rm <image> sh -lc 'cat /etc/passwd | tail -n +1 | head'
Conclusion
Corriger un « Permission denied » sur un volume Docker revient à résoudre une équation simple : l’utilisateur effectif (UID/GID) dans le conteneur doit avoir les droits nécessaires sur le répertoire réellement monté (bind mount côté host, ou volume géré par Docker).
Sur Linux, la solution la plus fiable est souvent l’alignement UID/GID (-u) ou un chown ciblé (ou ACL). Sur macOS, les particularités de Docker Desktop rendent les volumes nommés particulièrement utiles pour éviter les surprises, surtout pour les répertoires de dépendances/caches.
Si vous me donnez :
- votre OS (Linux distro / macOS),
- votre commande
docker runou votrecompose.yaml, - le message d’erreur exact,
- et la sortie de
docker exec ... id+ls -lddu chemin concerné,
je peux proposer une correction précise et minimale adaptée à votre cas.