Le mécanisme : pourquoi Docker refuse d'écrire
Docker partage le noyau Linux de l'hôte. Il n'existe pas de couche de virtualisation des utilisateurs : chaque processus dans un conteneur possède un UID et un GID réels, visibles depuis l'hôte. Quand un conteneur monte un répertoire hôte (bind-mount), le système de fichiers ne fait aucune magie : il applique les mêmes règles de permissions POSIX que pour n'importe quel processus local. Un fichier appartenant à l'UID 1000 sur l'hôte reste la propriété de l'UID 1000, peu importe que ce soit un utilisateur qui s'appelle alice côté hôte ou paperless côté conteneur. Si le processus dans le conteneur tourne sous l'UID 472 et que le répertoire appartient à root (UID 0), l'écriture est refusée — même si vous avez utilisé chmod 755.
Pourquoi les images n'utilisent-elles pas root par défaut ? La majorité des images Docker bien maintenues créent un utilisateur applicatif dédié pour respecter le principe de moindre privilège : un processus compromis ne peut pas modifier les binaires système du conteneur ni, si les options de sécurité sont correctes, accéder aux fichiers root de l'hôte. C'est une bonne pratique — mais elle introduit précisément ce décalage d'UID.
Le piège classique : vous créez le répertoire avec votre session SSH (UID 1000), puis vous lancez un conteneur Grafana qui tourne en UID 472. Grafana tente d'écrire sa base SQLite dans /var/lib/grafana monté depuis /opt/grafana/data — qui appartient à votre utilisateur SSH. Résultat : GF_PATHS_DATA='/var/lib/grafana' is not writable. Le conteneur démarre, répond sur le port 3000, mais toute configuration est perdue au redémarrage suivant car rien n'a jamais été écrit sur le disque.
Les applications les plus touchées et leur UID
Les problèmes de permissions Docker touchent principalement les applications qui s'exécutent avec un UID spécifique différent de celui de l'hôte.
- Paperless-ngx — UID
1000(utilisateurpaperless) : gère les répertoiresconsume,export,mediaetdata - Grafana — UID
472(utilisateurgrafana) : écrit sa base SQLite et ses plugins dans/var/lib/grafana - Nextcloud (image Debian) — UID
33(utilisateurwww-data) : répertoire de données, config et logs - Immich — UID
1000(utilisateurnode) : librairie photos, miniatures et base de données ML - Gitea — UID
1000(utilisateurgit) : dépôts, SSH keys, logs et base SQLite par défaut
Diagnostic : identifier le problème en moins de 2 minutes
Avant de corriger, confirmez que c'est bien un problème de permissions et identifiez l'UID en cause. Trois commandes suffisent.
Lire le message d'erreur exact
Consultez les logs du conteneur incriminé :
docker logs <nom-du-conteneur> 2>&1 | grep -i 'permission\|denied\|cannot\|mkdir'Un message
permission deniedoucannot create directoryconfirme le diagnostic.Identifier l'UID du processus dans le conteneur
docker exec <nom-du-conteneur> idSortie typique :
uid=472(grafana) gid=0(root). Notez l'UID — ici472.Vérifier le propriétaire du répertoire hôte
ls -ln /opt/grafana/dataLa sortie
drwxr-xr-x 2 0 0 ...indique que le répertoire appartient à root (UID 0). L'UID 472 n'a que les droits « other » — lecture et exécution, pas d'écriture.Vérifier avec stat pour un diagnostic complet
stat /opt/grafana/dataRegardez les lignes
Uid:etGid:. Si elles affichent(0/root)alors que votre conteneur tourne en 472, le problème est confirmé.Inspecter la configuration du conteneur
docker inspect <nom-du-conteneur> | grep -A5 'Mounts'Cette commande liste tous les bind-mounts et volumes nommés actifs, avec leur source sur l'hôte et leur destination dans le conteneur.
Correction cas par cas : chown sur le répertoire hôte
La correction de base est un chown du répertoire hôte vers l'UID attendu par le conteneur. Voici les commandes pour les applications les plus courantes.
Grafana (UID 472)
mkdir -p /opt/grafana/data chown -R 472:472 /opt/grafana/dataDans votre
compose.yml:volumes: - /opt/grafana/data:/var/lib/grafanaNextcloud (UID 33, image Debian)
mkdir -p /opt/nextcloud/{data,config,apps} chown -R 33:33 /opt/nextcloud/data chown -R 33:33 /opt/nextcloud/configAttention : l'image Alpine utilise l'UID 82. Vérifiez avec
docker exec <conteneur> id www-datasi vous n'êtes pas sûr de la variante utilisée.Paperless-ngx (UID 1000)
mkdir -p /opt/paperless/{consume,export,media,data} chown -R 1000:1000 /opt/paperless/consume chown -R 1000:1000 /opt/paperless/export chown -R 1000:1000 /opt/paperless/media chown -R 1000:1000 /opt/paperless/dataImmich (UID 1000)
mkdir -p /opt/immich/{library,thumbnails,encoded-video,profile} chown -R 1000:1000 /opt/immichNote : les variables d'environnement
PUID/PGIDne fonctionnent PAS avec les images officielles Immich. Elles sont spécifiques aux images LinuxServer.io.Vérifier après correction
ls -ln /opt/grafana/dataDoit afficher
drwxr-xr-x 2 472 472 .... Redémarrez ensuite le conteneur :docker compose restart grafana docker logs grafana --tail 20
Variantes selon l'app : bind-mounts, volumes nommés et user:
Le chown sur le répertoire hôte fonctionne pour les bind-mounts, mais Docker propose trois autres approches selon le contexte et l'image utilisée.
Directive user: dans compose.yml. Certaines images sont conçues pour accepter un UID arbitraire passé via user:. Cela évite le chown si votre répertoire appartient à votre utilisateur hôte :
services:
app:
image: mon-image
user: "1000:1000"
volumes:
- /home/user/data:/app/dataCette approche fonctionne uniquement si l'image ne requiert pas de fichiers internes appartenant à un UID spécifique (binaires SUID, sockets, etc.). Paperless-ngx supporte ce mode via sa variable USERMAP_UID ; Grafana ne le supporte pas proprement car son image modifie des permissions lors de son entrypoint. Si vous forcez user: 472:472 sur Grafana, le conteneur échoue au démarrage avec des erreurs sur /var/lib/grafana.
Variables d'environnement PUID/PGID. Les images de la communauté LinuxServer.io exposent les variables PUID et PGID : l'entrypoint de l'image reçoit un processus root, change dynamiquement l'UID de l'utilisateur applicatif avec usermod/groupmod, puis descend de privilèges. C'est pratique mais ces variables sont propres aux images LinuxServer.io — elles n'ont aucun effet sur les images officielles de Grafana, Nextcloud ou Immich. Ne les mélangez pas.
Volumes nommés. Avec un volume Docker nommé (docker volume create), Docker gère lui-même le répertoire sous /var/lib/docker/volumes/. À la première écriture, le répertoire est créé avec les permissions du processus du conteneur. Résultat : pas de problème de permissions au démarrage, mais la migration des données existantes nécessite une étape de copie explicite :
docker run --rm \
-v mon-ancien-repertoire:/from \
-v mon-volume-nomme:/to \
alpine sh -c 'cp -a /from/. /to/'Les volumes nommés conviennent bien aux nouvelles installations ; pour les données existantes, le bind-mount avec un chown préalable reste plus prévisible.
Cas NFS et montages distants
Les montages NFS et CIFS (partages Samba/Windows) ajoutent une couche de complexité car l'arbitrage des permissions se fait à deux endroits : le système de fichiers local ET le serveur distant. Comprendre lequel des deux bloque est essentiel avant de chercher une correction.
Problème NFS — root_squash. Par défaut, un serveur NFS exporte avec root_squash : tout accès qui arrive avec UID 0 (root) est automatiquement réattribué à l'utilisateur nobody (UID 65534). Si votre conteneur tourne en UID 472 et que le partage NFS n'a pas de mapping UID côté serveur, le client monte le partage, voit les fichiers, mais l'écriture est refusée — parce que le serveur NFS considère que l'UID 472 du client n'a pas les droits sur ce partage.
Solutions :
1. Configurer l'export NFS avec all_squash,anonuid=472,anongid=472 pour Grafana, ou l'UID correspondant à votre application.
2. Créer un utilisateur côté serveur NFS avec le même UID que dans le conteneur et lui attribuer la propriété des répertoires partagés.
3. Utiliser no_root_squash uniquement si vous contrôlez complètement le réseau interne — cette option permet à root sur le client d'agir comme root sur le serveur, ce qui est un risque de sécurité significatif.
Problème CIFS — options de montage figées. Contrairement à ext4 ou XFS, un montage CIFS ne supporte pas le chown après montage : la propriété est fixée par les options passées à la commande de montage. Si vous montez un partage Windows sans spécifier d'UID, les fichiers apparaissent en root (UID 0) et les conteneurs Nextcloud (UID 33) ne peuvent pas y écrire, quelle que soit la permission POSIX visible côté client.
Pour les montages CIFS dans /etc/fstab :
//serveur/partage /opt/nextcloud/data cifs uid=33,gid=33,credentials=/etc/cifs-creds,iocharset=utf8 0 0Vérifiez toujours que le fichier de credentials (/etc/cifs-creds) n'est lisible que par root (chmod 600).
Méthodes de correction : comparaison
Faites défiler le tableau
| Méthode | Avantages | Risques / Limites |
|---|---|---|
| chown UID:GID sur le répertoire hôte | Simple, universelle, compatible toutes images | Nécessite de connaître l'UID exact ; à refaire si le répertoire est recréé |
| user: UID:GID dans compose.yml | Pas de chown à gérer ; portable entre hôtes | L'image doit supporter les UIDs arbitraires ; peut casser des fichiers internes |
| Volumes nommés Docker | Permissions gérées automatiquement au premier démarrage | Moins lisible ; migration de données existantes plus complexe |
| --privileged ou chmod 777 | Résout le problème immédiatement | DANGER : expose l'hôte et tous ses processus ; ne jamais utiliser en production |
Dépannage : 4 erreurs classiques avec leur message exact
Voici les quatre erreurs Docker les plus courantes liées aux permissions, avec le message d'erreur exact et sa solution.
mkdir: cannot create directory '/var/lib/grafana/plugins': Permission denied→ Le répertoire hôte n'appartient pas à l'UID 472. Appliquezchown -R 472:472 /opt/grafana/data.[Errno 13] Permission denied: '/usr/src/paperless/media'→ Paperless-ngx ne peut pas écrire dans son répertoire media. Vérifiez que le bind-mount appartient à l'UID 1000 sur l'hôte.Could not create lock file /var/lib/grafana/.~lock.grafana.db→ Grafana peut lire le répertoire mais pas y écrire. Problème de permissions sur les fichiers existants :chown 472:472 /opt/grafana/data/*.dbou supprimez le lock file.chown: changing ownership of '/data': Operation not permitted(au démarrage du conteneur) → L'image tente elle-même de corriger les permissions mais ne peut pas car elle ne tourne pas en root. Effectuez lechownmanuellement sur l'hôte AVANT de démarrer le conteneur.
SELinux et AppArmor : les flags :z et :Z dans les bind-mounts. Sur les systèmes avec SELinux actif (CentOS, RHEL, Fedora), un bind-mount peut être bloqué même si les permissions POSIX sont correctes. Docker fournit deux suffixes : :z relabellise le contenu pour le partager entre plusieurs conteneurs, et :Z relabellise en accès privé (un seul conteneur). Exemple : - /opt/grafana/data:/var/lib/grafana:z. Sans ce flag sur un système SELinux, vous obtiendrez Permission denied même après un chown correct. Pour vérifier si SELinux bloque : ausearch -m AVC -ts recent | grep docker.
Conclusion
Le Permission denied dans les logs Docker n'est jamais une fatalité. La solution tient en trois commandes : docker exec <conteneur> id pour connaître l'UID, ls -ln <répertoire-hôte> pour confirmer le propriétaire actuel, puis chown -R <UID>:<GID> <répertoire-hôte> pour corriger. La règle d'or : créer les répertoires hôtes avec le bon propriétaire avant de démarrer le conteneur, pas après.
Pour éviter de reproduire le problème à chaque nouvelle installation, documentez les UIDs dans votre compose.yml sous forme de commentaires et intégrez le chown dans votre script de provisionnement ou votre playbook Ansible. Un rôle Ansible qui crée les répertoires de données avec owner: 472 et group: 472 (pour Grafana) avant le docker compose up élimine la cause à la racine.
Sur un VPS dédié, cette discipline évite 90 % des incidents de données silencieusement perdues au redémarrage — des incidents qui ne produisent aucune alerte car le conteneur est vert, le service répond, mais tout ce qui a été configuré depuis le dernier démarrage a disparu. Un chown de deux secondes avant le premier lancement vous en épargne des heures de débogage.