← Retour aux tutoriels

Corriger les erreurs « Permission denied » sur les volumes Docker (Linux & macOS)

dockervolumespermissionslinuxmacosuid-gidbind-mountselinuxapparmordevops

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

1.1. UID/GID : la vraie identité, pas le nom

Sous Linux (et dans la VM Linux de Docker Desktop), les permissions reposent sur :

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

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 :

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 :

Le message vous donne souvent :

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 :


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 :


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 :

Inconvénients :


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

Vérifier :

getfacl ./var | sed -n '1,80p'

Quand utiliser ACL ?


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

Vérifier l’état :

getenforce

4.5.2. Corriger avec :Z ou :z sur le bind mount

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 :

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 :

Vérifier si vous êtes en rootless :

docker info | sed -n '/Rootless/p'

Stratégies :


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 :

La règle pratique : sur macOS, évitez de compter sur chown sur un bind mount comme solution universelle. Préférez :

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 :

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

Approche recommandée (dev)

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

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

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

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) :

  1. trouver l’UID :
docker run --rm nginx:alpine sh -lc 'id nginx || id www-data'
  1. appliquer chown sur 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

  1. Quel chemin provoque l’erreur ? (message d’erreur exact)
  2. Est-ce un bind mount ou un volume nommé ?
    • bind mount → permissions host cruciales
    • volume nommé → permissions gérées côté Docker
  3. Dans le conteneur :
    • id (UID/GID)
    • ls -ld <chemin>
    • test touch <chemin>/test
  4. Sur Linux host (si bind mount) :
    • stat du répertoire
    • ajuster via chown / chmod / ACL
  5. Sur Fedora/RHEL/CentOS :
    • tester :Z sur le mount (SELinux)
  6. 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 »

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 :

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 :

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 :