À quoi servent PUID et PGID dans Docker Compose ?
PUID et PGID ne sont pas des paramètres Docker : dans les images linuxserver.io, ils évitent les fichiers de bind mount en 911:911. Voici comment corriger cela.
Ce que sont réellement PUID et PGID
PUID et PGID sont deux variables d’environnement que certaines images de conteneur lisent au démarrage. Docker lui-même ne les examine jamais. Il s’agit d’une convention utilisée par les images linuxserver.io et quelques autres. Une image qui n’a pas été conçue pour les lire les ignore silencieusement.
Dans une image linuxserver.io, un utilisateur appelé abc est créé lors du build avec l’UID (user ID) 911 et le GID (group ID) 911. Le conteneur démarre en tant que root, exécute ses scripts d’initialisation, puis l’un de ces scripts renumérote cet utilisateur avant toute autre opération :
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abcL’option -o autorise l’utilisation d’un ID déjà utilisé ailleurs. Ensuite, le script d’initialisation abandonne les privilèges root et exécute l’application en tant que abc. Docker transmet donc PUID=1000 au conteneur comme n’importe quelle autre variable d’environnement et n’agit jamais sur sa valeur. Cette variable renumérote un utilisateur dans le conteneur avant le démarrage de l’application. Avec un daemon rootful standard, tous les fichiers écrits par cette application sont donc créés sur votre disque avec le propriétaire 1000. Docker rootless décale cette valeur, comme expliqué plus bas. Si vous ne définissez pas PUID, abc conserve la valeur 911. C’est pourquoi un bind mount non configuré se remplit de fichiers appartenant à 911:911.
Obtenez vos deux identifiants avec id
Exécutez cette commande sur l’hôte, avec le compte qui possède les répertoires de données :
iduid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),988(docker)uid correspond à votre PUID et gid à votre PGID. Dans un script, id -u et id -g affichent uniquement les valeurs numériques. Sur la plupart des images VPS fraîchement installées, le premier compte utilisateur est 1000:1000, mais ne le supposez pas. Après une reconstruction du serveur ou l’ajout ultérieur d’un deuxième compte, la valeur peut être 1001 ou plus. Une valeur incorrecte suffit à provoquer tout le problème. Si vos services s’exécutent avec un compte de service dédié plutôt qu’avec votre propre compte de connexion, exécutez id thatuser et utilisez les valeurs obtenues.
Pourquoi vos fichiers apparaissent comme 911:911
ls -l affiche un identifiant numérique au lieu d’un nom lorsqu’aucun compte de l’hôte ne correspond à cet identifiant. Aucun compte de votre serveur n’a l’UID 911, donc aucun nom ne peut être affiché. Utilisez ls -ln pour afficher systématiquement les identifiants numériques et lever toute ambiguïté :
ls -ln /srv/appdata/sonarrdrwxr-xr-x 2 911 911 4096 Aug 7 09:12 Backups
-rw-r--r-- 1 911 911 512 Aug 7 09:12 config.xmlCette sortie indique que le conteneur a utilisé les valeurs par défaut intégrées. Le fichier config.xml de cette liste contient également les paramètres d’authentification. C’est important la première fois que vous ouvrez l’interface web et constatez que Sonarr et Radarr sont livrés sans nom d’utilisateur ni mot de passe par défaut. Vérifiez-le depuis l’intérieur du conteneur au lieu de faire une supposition :
docker exec sonarr id abc
docker compose logs sonarr | head -n 25Le script d’initialisation linuxserver affiche son résultat dans le startup log, sur 2 lignes :
User UID: 911
User GID: 911Si ces lignes affichent 911 après la définition de PUID=1000 dans votre fichier Compose, le conteneur en cours d’exécution n’utilise pas la valeur définie. La cause habituelle est que vous avez modifié docker-compose.yml, puis exécuté docker compose restart, qui réutilise le conteneur existant avec son environnement d’origine. Les changements d’environnement nécessitent docker compose up -d, qui recrée le conteneur. L’autre cause est intentionnelle : lorsqu’une image s’exécute en mode read-only ou non-root, le script d’initialisation ignore entièrement la renumérotation et affiche l’ID inchangé de abc, même si la variable est présente dans le conteneur.
Pourquoi vous ne pouvez pas supprimer un fichier écrit par le conteneur
Le kernel compare des numéros, jamais des noms. Votre shell s’exécute avec l’UID 1000. Le fichier appartient à l’UID 911. Le répertoire qui le contient est drwxr-xr-x et appartient également à 911. Le groupe et les autres utilisateurs disposent donc des permissions de lecture et d’exécution, mais pas d’écriture. Pour supprimer un fichier, vous devez disposer de la permission d’écriture sur son répertoire, pas sur le fichier lui-même. Vous obtenez donc cette erreur même lorsque le fichier semble inoffensif :
rm: cannot remove '/srv/appdata/sonarr/config.xml': Permission deniedUn conteneur qui écrit rencontre le même problème dans l’autre sens. Si le répertoire de l’hôte appartient à votre utilisateur avec le mode 755 et que l’application s’exécute avec l’UID 911, sa première écriture échoue avec Permission denied et l’application le signale avec ses propres termes. Dans une application .NET comme Sonarr ou Radarr, cela apparaît sous la forme UnauthorizedAccessException: Access to the path '/data/downloads' is denied. La chaîne de permissions placée devant le fichier vous indique lequel des trois ensembles de permissions s’applique réellement à votre utilisateur. Lire correctement drwxr-xr-x permet de comprendre immédiatement cette erreur.
Il s’agit spécifiquement d’un problème de bind mount. Lorsque Docker crée un named volume vide et le monte sur un chemin qui existe dans l’image, il copie le contenu de ce chemin dans le volume, y compris le propriétaire et les bits de permission. L’application trouve donc un répertoire dont elle est déjà propriétaire. Un bind mount ne bénéficie pas de ce traitement : Docker monte le répertoire de l’hôte exactement tel qu’il est. Cette différence explique notamment pourquoi il est utile de savoir quand un bind mount est préférable à un named volume et quand il ne l’est pas.
Corriger un répertoire dont les permissions sont déjà incorrectes
Définir PUID et PGID modifie le comportement de l’application à partir de maintenant. Cela ne corrige pas rétroactivement les fichiers déjà présents sur le disque. Arrêtez la stack, corrigez vous-même le propriétaire, puis redémarrez-la :
docker compose down
sudo chown -R 1000:1000 /srv/appdata/sonarr
docker compose up -dUtilisez sudo chown -R "$(id -u):$(id -g)" /srv/appdata/sonarr si vous préférez ne pas saisir les nombres. Faites-le lorsque le conteneur est arrêté, car une application en cours d’écriture pendant un chown récursif peut laisser une arborescence partiellement corrigée et provoquer une nouvelle série d’erreurs difficiles à comprendre.
Ce que PUID et PGID ne corrigent pas
Voici le point qui piège les personnes qui ont pourtant tout configuré correctement. Au démarrage, l’init de linuxserver exécute un chown sur exactement trois chemins : /app, /config et /defaults. Vos montages de médias n’en font pas partie. /data, /downloads et /tv sont transmis à l’application sans modification. Si le côté hôte de ces montages appartient à un utilisateur auquel l’utilisateur du conteneur ne peut pas écrire, le conteneur démarre normalement, affiche le bon UID dans sa bannière, puis échoue lors du premier import.
C’est le comportement attendu. Exécuter un chown récursif sur une bibliothèque de médias de douze téraoctets à chaque démarrage du conteneur serait désastreux. Les répertoires de médias sont donc sous votre responsabilité. Ce sont aussi les montages où les permissions posent réellement problème. Ce type d’échec apparaît discrètement dans le journal de l’application, plusieurs heures après que le conteneur a semblé sain. Un test d’écriture périodique relié à ntfy sur votre propre VPS, avec des alertes envoyées sur votre téléphone est un moyen simple d’être averti avant qu’une semaine d’épisodes manquants ne révèle le problème.
Trois façons de contrôler l’utilisateur, et dans quels cas les utiliser
Variables d’environnement PUID et PGID
Cette méthode fonctionne uniquement avec les images dont l’entrypoint lit ces variables. Elle est populaire, car le conteneur démarre toujours en tant que root, effectue sa propre initialisation, corrige /config, puis abandonne les privilèges. Les Docker Mods et les scripts d’initialisation personnalisés continuent de fonctionner. En contrepartie, vous faites confiance à une convention plutôt qu’à une fonctionnalité de la plate-forme, et les noms des variables ne sont pas standardisés entre les projets.
La clé user: dans Compose
Il s’agit d’une véritable fonctionnalité Docker. Elle fonctionne avec toutes les images, car le runtime du conteneur l’applique avant l’exécution du code de l’image :
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
user: "1000:1000"Le processus ne s’exécute jamais en tant que root, même brièvement. C’est un véritable gain de sécurité. En revanche, cette méthode casse tout ce qui nécessite les privilèges root dans l’entrypoint. Sur les images linuxserver, le projet prend en charge cette configuration dans la mesure du raisonnable et uniquement pour les images qu’il a testées. Les restrictions sont précises : PUID et PGID n’ont plus aucun effet, les Docker Mods ne s’exécutent pas, les services personnalisés ne s’exécutent pas et vous devez gérer les permissions de chaque volume monté. Le modèle documenté associe cette option à un /run accessible en écriture :
user: 1000:1000
tmpfs:
- /run:uid=1000,gid=1000,exec
security_opt:
- no-new-privileges=trueUn effet purement esthétique peut surprendre. Un user: numérique sans entrée correspondante dans le /etc/passwd du conteneur fait afficher whoami: cannot find name for user ID 1000 par les outils. L’identifiant est valide et l’accès aux fichiers fonctionne normalement. Seule la recherche du nom échoue.
Docker rootless
Docker rootless exécute le daemon lui-même avec votre utilisateur sans privilèges. Aucun processus sur la machine ne s’exécute donc réellement en tant que root. Cela modifie complètement le calcul des propriétaires. L’UID 0 du conteneur correspond à l’UID de l’utilisateur qui exécute Docker rootless sur l’hôte. L’UID n du conteneur, pour toute n supérieure ou égale à 1, correspond à subuid + (n - 1), où subuid est le début de la plage qui vous est attribuée dans /etc/subuid et /etc/subgid. Docker attend au moins 65,536 identifiants secondaires à cet emplacement.
Relisez ce mapping, car il inverse les recommandations habituelles. Avec Docker rootless, un conteneur qui écrit en tant que root crée des fichiers dont vous êtes propriétaire. Un conteneur qui écrit avec l’UID 1000 crée des fichiers appartenant à un identifiant secondaire proche de 100999, auxquels votre shell ne peut pas accéder. La valeur PUID correcte avec un daemon rootful est donc incorrecte ici. Les deux mécanismes résolvent le même problème, mais à des niveaux différents. Les combiner sans vérifier leur fonctionnement est le meilleur moyen de se retrouver avec un répertoire qu’il faut sudo pour supprimer. Si vous passez à rootless, testez la propriété d’un fichier créé sur votre propre serveur avant d’y migrer une bibliothèque.
Pour la plupart des stacks auto-hébergées sur un VPS unique, PUID et PGID avec un daemon rootful constituent le choix pragmatique, car c’est la configuration prévue et documentée pour ces images. Utilisez user: lorsque le README de l’image indique qu’elle a été testée avec cette option, ou lorsque vous exécutez une image officielle upstream qui ne prend pas du tout en charge PUID. Un espace de travail documentaire tel que une instance AFFiNE auto-hébergée sur un VPS relève du second cas, car aucun de ses conteneurs ne lit PUID. La propriété de son répertoire de base de données et de ses fichiers importés est donc déterminée par le runtime, et non par une variable du bloc environment. Il en va de même pour un centre de support Chatwoot auto-hébergé, où le conteneur Rails et son worker Sidekiq écrivent tous deux dans un même répertoire d’imports. Aucun des deux ne lit PUID : ce répertoire doit donc correspondre à l’utilisateur avec lequel l’image s’exécute déjà. Rien ne change avec une stack plus récente. Ainsi, attribuer à chaque membre de l’équipe son propre agent OneCLI isolé laisse les répertoires de travail individuels et le répertoire de données Postgres appartenir à l’utilisateur avec lequel chaque image s’exécute déjà. Il s’agit donc d’un problème de user: et de chown, et non d’un problème de PUID. Si vous placez une seule API auto-hébergée devant Codex, Claude Code et Hermes, vous conservez la même organisation, car cette image s’exécute avec son propre utilisateur intégré et le bind mount contenant sa base de données et ses clés stockées prend la propriété associée à cet utilisateur.
Le cas d’une stack multimédia : un groupe partagé entre les conteneurs
Une stack multimédia arr avec Sonarr, Radarr et un client de téléchargement permet de mettre ces principes en pratique. Le client de téléchargement écrit un fichier terminé dans /data/downloads. Sonarr crée ensuite un hardlink vers ce fichier ou le déplace dans /data/media. Pour que le hardlink fonctionne, les deux conteneurs doivent avoir un accès en écriture au même arbre. Si le client de téléchargement s’exécute avec l’UID 1000 tandis que Sonarr utilise l’UID 1001, l’un possède les fichiers que l’autre peut seulement lire.
La solution consiste à utiliser un groupe partagé dont chaque conteneur de la stack utilise le PGID :
sudo groupadd -g 13000 media
sudo usermod -aG media deploy
sudo chown -R deploy:media /srv/media
sudo find /srv/media -type d -exec chmod 2775 {} +
sudo find /srv/media -type f -exec chmod 0664 {} +Le 2 initial de 2775 correspond au bit setgid. Sur un répertoire, il indique que chaque nouveau fichier et sous-répertoire créé à l’intérieur hérite du groupe media au lieu du groupe principal du créateur. Cette configuration reste donc valable pour les nouveaux téléchargements sans devoir réexécuter chown. Déconnectez-vous puis reconnectez-vous, ou exécutez newgrp media, avant de vérifier vos propres accès : un groupe ajouté avec usermod -aG n’apparaît pas dans une session shell déjà ouverte.
Dans le conteneur, groupmod -o -g 13000 abc renumérote le groupe abc avec le GID 13000. Ainsi, abc écrit avec le même GID que le groupe media de l’hôte. Chaque conteneur de la stack conserve son propre PUID et partage ce PGID. Cela inclut les conteneurs situés plus loin dans la chaîne qui lisent uniquement la bibliothèque terminée, comme Jellyfin lui-même et les interfaces que l’on lui ajoute, par exemple Halcyon, qui sert cette bibliothèque sous la forme d’un vidéoclub vidéo des années 90 que l’on peut parcourir.
Définissez ensuite UMASK=002 pour chaque conteneur linuxserver de la stack. C’est l’étape souvent oubliée. Si cette variable n’est pas définie, les images n’appliquent aucun umask spécifique. L’application conserve donc l’umask dont elle a hérité. La valeur d’exemple indiquée dans les README des images est UMASK=022. Un umask 022 retire le bit d’écriture du groupe. Les fichiers sont donc créés avec les permissions 0644 : les autres membres du groupe peuvent encore les lire, mais pas les modifier. Cela annule l’essentiel du partage que vous venez de configurer. Avec 002, une application qui demande les permissions habituelles 0666 pour les fichiers et 0777 pour les répertoires obtient des fichiers 0664 et, dans l’arbre setgid, des répertoires 2775 ; le groupe peut alors écrire. Un umask ne fait que retirer des bits. Une application qui demande 0600 obtient donc toujours 0600. De même, un répertoire doté d’une ACL par défaut ignore complètement l’umask. Ajoutez la variable à côté de PUID et PGID :
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=Etc/UTC
volumes:
- /srv/appdata/sonarr:/config
- /srv/media:/data
restart: unless-stoppedCes deux valeurs doivent être placées dans un fichier .env à côté du fichier Compose, afin que toute la stack utilise une seule définition :
PUID=1000
PGID=13000Compose lit automatiquement ce fichier pour effectuer la substitution de variables de type ${PUID}. C’est le même mécanisme que celui utilisé pour les identifiants. Les bonnes pratiques consistant à sortir les valeurs de docker-compose.yml et à les placer dans un fichier .env s’appliquent également ici. La différence est que ces deux nombres ne sont pas secrets.
Vérifiez le fonctionnement de bout en bout au lieu de faire confiance à la configuration. Écrivez un fichier depuis un conteneur, puis lisez-le depuis l’hôte :
docker exec sonarr touch /data/downloads/permtest
ls -ln /srv/media/downloads/permtestUn résultat correct affiche votre PUID comme propriétaire, 13000 comme groupe et -rw-rw-r-- comme mode. Si le groupe est affiché comme 1000, exécutez ls -ldn /srv/media/downloads : soit le bit setgid, c’est-à-dire le s dans drwxrwsr-x, manque sur ce répertoire, soit le répertoire appartient encore au groupe 1000 au lieu de media. Les commandes chown et chmod 2775 ci-dessus corrigent ces deux problèmes. Si le mode affiché est -rw-r--r--, la variable UMASK n’a pas été prise en compte. Vérifiez donc que vous avez recréé le conteneur au lieu de simplement le redémarrer. Supprimez le fichier de test avec rm /srv/media/downloads/permtest lorsque vous avez terminé.
Quelles images utilisent quelles variables
Les images linuxserver.io utilisent PUID, PGID et UMASK. Paperless-ngx utilise des noms différents pour la même notion : USERMAP_UID et USERMAP_GID, tous deux définis par défaut sur 1000. Sa documentation vous indique de les lire depuis id -u et id -g. Les serveurs photo présentent la même diversité : PhotoPrism possède sa propre paire PHOTOPRISM_UID et PHOTOPRISM_GID, tandis qu’Immich ne fournit aucun équivalent et laisse Docker utiliser la clé user: pour l’utilisateur du conteneur. Choisir entre PhotoPrism et Immich détermine donc aussi le mécanisme que vous devrez maintenir pour la plus grande bibliothèque du serveur. De nombreuses images officielles en amont, notamment les images courantes de bases de données et de serveurs web, utilisent un utilisateur intégré fixe et attendent que vous utilisiez user: ou que vous le laissiez inchangé. Les petits déploiements d’une seule application soulèvent la même question. Ainsi, lorsque vous installez le gestionnaire d’entraînement openGym auto-hébergé, vérifiez sous quel utilisateur le conteneur s’exécute réellement avant de lui associer un bind mount. Le répertoire qui contient sa base de données dépend de cette configuration, que vous définissiez ou non PUID. Il en va de même pour un relais d’accès distant. Lorsque vous exécutez votre propre serveur relais RustDesk, la paire de clés Ed25519 que hbbs écrit au premier démarrage arrive dans votre bind mount avec le propriétaire correspondant à l’utilisateur utilisé par l’image. Vous pouvez corriger cela ensuite avec un chown exécuté sur l’hôte, ou choisir l’identité dès le départ avec la clé user:, car cette image ne définit pas elle-même d’utilisateur, à condition que le répertoire monté soit accessible en écriture par l’ID choisi. Le même principe s’applique aux composants d’infrastructure que vous ajoutez plus tard. Placer Authentik devant vos applications pour utiliser une seule connexion signifie exécuter les images officielles du serveur, de Postgres et de Redis. Elles ne lisent aucune variable PUID. Le propriétaire des volumes dépend donc du runtime, et non d’un entrypoint que vous pouvez configurer.
Consultez donc le README de chaque image avant de copier un bloc d’environnement d’un projet à l’autre. Docker transmet toute variable d’environnement que vous définissez à n’importe quel conteneur, même si aucun composant interne ne la lit. Un PUID qui n’est consommé par rien ne produit ni erreur, ni avertissement, ni effet. Le conteneur s’exécute avec l’utilisateur défini dans son propre Dockerfile, et vous pouvez l’identifier en examinant le propriétaire des fichiers qu’il crée. Effectuez cette vérification avant d’ajouter quoi que ce soit au serveur, notamment une stack d’analyse de sécurité open-kritt auto-hébergée. Le fichier Compose vous indique si les images prennent en charge PUID ou si le propriétaire des répertoires montés est fixé par les images elles-mêmes.
FAQ
Pourquoi mes fichiers Docker appartiennent-ils à 911:911 ?
911 est l’UID et le GID de l’utilisateur abc intégré aux images linuxserver.io. Cela signifie que le conteneur a démarré sans définir PUID et PGID. Son script d’initialisation a donc conservé les valeurs par défaut intégrées. ls -l affiche les nombres bruts, car aucun compte de votre hôte n’a l’ID 911. Aucun nom ne peut donc être affiché. Définissez PUID et PGID avec la sortie de id, recréez le conteneur avec docker compose up -d, puis corrigez les fichiers existants avec sudo chown -R 1000:1000 dans le répertoire concerné.
PUID et PGID fonctionnent-ils avec toutes les images Docker ?
Non. Ce ne sont pas des fonctionnalités de Docker, et Docker ne les lit jamais. Ils fonctionnent uniquement avec les images dont le propre entrypoint les lit et appelle usermod et groupmod avant de démarrer l’application. C’est le cas de la famille linuxserver.io et de quelques projets qui ont repris ce modèle. D’autres projets utilisent d’autres noms, comme USERMAP_UID et USERMAP_GID dans paperless-ngx. Avec une image qui ne lit aucune de ces variables, elles sont acceptées puis ignorées sans avertissement.
Dois-je utiliser PUID et PGID ou la clé user: dans Docker Compose ?
Utilisez PUID et PGID lorsque l’image les prend en charge. L’entrypoint s’exécute alors encore suffisamment longtemps avec root pour corriger /config et démarrer correctement ses propres services. Utilisez user: lorsque l’image ne prend pas en charge PUID, ou lorsque son README indique qu’elle est testée avec un fonctionnement sans root. Avec une image linuxserver, définir user: rend PUID et PGID inopérants, empêche l’exécution des Docker Mods et des services personnalisés, et vous rend responsable des permissions de chaque volume monté.
Sonarr utilise le bon PUID, mais ne peut toujours pas déplacer les fichiers. Quel est le problème ?
Vérifiez trois éléments, dans cet ordre. Premièrement, le montage des médias lui-même : le script d’initialisation applique uniquement chown à /app, /config et /defaults. Ainsi, /data ou /downloads conserve le propriétaire défini sur l’hôte. Deuxièmement, le groupe partagé : si le client de téléchargement et Sonarr utilisent des GID différents, aucun des deux ne peut modifier les fichiers de l’autre. Attribuez donc le même PGID à chaque conteneur de la stack. Troisièmement, l’umask : si UMASK n’est pas défini ou vaut 022, les nouveaux fichiers sont créés avec 0644, sans droit d’écriture pour le groupe. Les autres conteneurs du groupe peuvent alors les lire, mais pas les modifier. Définissez UMASK=002 et activez le bit setgid sur les répertoires avec chmod 2775 afin que les nouveaux fichiers héritent du groupe.