Synchroniser l'utilisateur DevContainer avec l'hôte — La bonne méthode (UID/GID + Nom d'utilisateur)

Synchroniser l’utilisateur DevContainer avec l’hôte — La bonne méthode (UID/GID + Nom d’utilisateur)

Table des matières

Si vous avez déjà travaillé avec les DevContainers, vous avez sûrement rencontré ce problème à un moment :

« Pourquoi mes permissions de fichiers cassent d’un coup ? »

En général, ça démarre simplement.

Vous créez un fichier dans un DevContainer. Vous repassez sur votre terminal hôte. Vous essayez d’éditer le fichier. Et vous tombez sur une erreur EACCES.

Ou Git vous dit que certains fichiers appartiennent à root alors que vous n’avez jamais touché sudo.

Ça casse complètement le rythme. Et la cause est presque toujours la même :

L’utilisateur du DevContainer ne correspond pas à l’utilisateur de l’hôte.

Dans cet article, vous verrez exactement comment corriger ça, avec une méthode qui fonctionne :

  • ✔ sur n’importe quelle image de base
  • ✔ avec ou sans Dockerfile
  • ✔ synchronisation du nom d’utilisateur facultative
  • ✔ synchronisation UID/GID indispensable
  • ✔ images multi-utilisateurs
  • ✔ images DevContainer préconfigurées

Vous apprendrez des modèles simples qui garantissent une synchronisation propre à chaque fois. Pas de bricolage. Pas d’approximations.

Info

Déjà familier avec les problèmes UID/GID dans les DevContainers ?

La solution universelle consiste à garantir que votre image contient un seul utilisateur régulier, dont l’UID/GID et le nom peuvent être alignés par DevContainers.

Passez directement à la solution : Approche universelle recommandée (compatible toutes images)

Pourquoi la synchronisation utilisateur est essentielle (et pourquoi ça casse)

Les DevContainers semblent simples. Mais quand l’utilisateur interne ne correspond pas à celui de l’hôte, vous obtenez :

  • Propriété de fichier incorrecte : Les fichiers créés dans un montage bind ou un volume sont souvent attribués au root de l’hôte ou à un UID non aligné.

  • Erreurs “Permission denied” : Votre éditeur ne peut plus sauvegarder. Vos outils ne peuvent plus modifier les fichiers générés.

  • Comportement incohérent en CI/CD : Les permissions locales divergent de celles en pipeline.

Voici un exemple concret.

Exemple : DevContainer Ubuntu 24.04 exécuté en root

devcontainer.json :

1{
2  "image": "ubuntu:24.04"
3}

VS Code lance le conteneur. Par défaut, le conteneur tourne en root.

Dans le conteneur :

1touch test.txt

Sur l’hôte :

1ls -l test.txt
2# -rw-r--r-- 1 root root 0 Dec  8 07:00 test.txt

Impossible de modifier ce fichier normalement. Votre éditeur refuse d’écrire dedans. Git peut signaler une “propriété dangereuse”.

C’est exactement pour ça que la synchronisation utilisateur est cruciale.

Les deux objectifs que nous voulons atteindre

Il y a deux objectifs distincts :

1. UID/GID identiques entre hôte et conteneur (obligatoire)
Sans ça, les permissions casseront. C’est la seule vraie exigence technique.
2. Nom d’utilisateur identique (optionnel mais conseillé)
Ça améliore la cohérence : dotfiles, prompts, clés SSH…

Comment fonctionnent les réglages utilisateurs des DevContainers

Les DevContainers utilisent :

  • remoteUser
  • containerUser
  • updateRemoteUserUID / updateContainerUserID

(Documentation : https://containers.dev/implementors/json_reference/)

Pour que la synchro UID/GID fonctionne :

  1. L’utilisateur défini dans remoteUser / containerUser doit exister dans l’image
  2. Cet utilisateur doit avoir un vrai dossier home
  3. Aucun autre utilisateur ne doit partager l’UID/GID de l’hôte
  4. Ensuite updateContainerUserID aligne automatiquement UID/GID

Si un autre utilisateur a déjà l’UID de l’hôte, la mise à jour est impossible.

Exemple de collision UID

Image avec deux utilisateurs :

Nom UID
vscode 1000
bob 1001

Hôte : UID = 1000, username = bob.

Dans devcontainer.json :

1{
2  "remoteUser": "bob",
3  "updateContainerUserID": true
4}

Le DevContainer essaie de basculer bob → UID 1000. Mais vscode utilise déjà 1000. Conflit. Mise à jour impossible. Le conteneur reste avec UID 1001. L’hôte a UID 1000. Les permissions cassent.

Règle générale pour des résultats fiables

Pour que la synchronisation soit toujours fiable :

Votre image doit contenir exactement un seul utilisateur régulier (non root, non système).

Cet utilisateur doit :

  1. être défini comme utilisateur DevContainer par défaut (c’est lui qui sera réellement utilisé à l’exécution)
  2. disposer d’un répertoire home valide et correctement configuré (indispensable pour un utilisateur fonctionnel)
  3. être le seul utilisateur régulier dans l’image (pour éliminer tout risque de conflit UID/GID)

Warning

Root reste root.
Les utilisateurs système ne doivent pas être modifiés.
Certains utilisateurs comme nobody n’ont même pas de home.

Si vous voulez aussi synchroniser le nom : Ce seul utilisateur doit porter le même nom que celui de l’hôte.

Résultat :

  • portable
  • indépendant de l’image de base
  • prévisible
  • sans conflits

Approche universelle privilégiée (indépendante de l’image de base)

Cette méthode fonctionne partout.

Elle garantit :

  • un seul utilisateur régulier
  • nom identique à celui de l’hôte
  • synchronisation UID/GID automatique
  • aucun conflit multi-utilisateur
  • dossier home propre
  • fonctionne quels que soient les contenus de l’image de base

Nous appliquons cette approche universelle pour couvrir les deux configurations possibles :

  1. Avec un Dockerfile (par exemple lors de l’utilisation d’un Dockerfile partagé pour dev + prod)

  2. Sans Dockerfile → utilisez une Feature DevContainer locale pour créer/synchroniser l’utilisateur. Les lifecycle scripts ne peuvent pas être utilisés, car ils ne modifient pas l’image.

Le principe : exécuter ce script lors du build final du DevContainer. USERNAME = nom d’utilisateur de l’hôte.

 1#!/bin/bash
 2set -e
 3
 4# Supprimer tout utilisateur régulier existant.
 5# - la commande `getent` liste tous les utilisateurs
 6# - la commande `awk` filtre les utilisateurs réguliers (UID >= 1000 et n'est pas "nobody")
 7# - la commande `xargs` supprime ces utilisateurs
 8getent passwd \
 9  | awk -F: '($3 >= 1000) && ($1 != "nobody") {print $1}' \
10  | xargs -r -n 1 userdel -r
11    
12# Créer l'utilisateur correspondant à l'hôte et l'ajouter aux sudoers et autres groupes nécessaires :
13# - nom d'utilisateur `USERNAME`
14# - UID 1000
15# - GID 1000
16# - gracieusement sauter si root
17if [ "${USERNAME}" != "root" ]; then
18  groupadd --gid 1000 ${USERNAME} || true \
19    && useradd -s /bin/bash -m -u 1000 -g 1000 ${USERNAME} \
20    && mkdir /etc/sudoers.d/ \
21    && echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} \
22    && chmod 0440 /etc/sudoers.d/${USERNAME}
23fi

Choisissez l’onglet correspondant à votre setup.

  • Avec un Dockerfile
  • Sans Dockerfile

Voici une représentation visuelle de la structure du répertoire :

project-root/
└── .devcontainer/
    ├── devcontainer.json
    └── Dockerfile

devcontainer.json :

 1{
 2  "build": {
 3    "dockerfile": "Dockerfile",
 4    "args": {
 5      // Passer le nom d'utilisateur de l'hôte en argument de build
 6      "USERNAME": "${localEnv:USER}"
 7    }
 8  },
 9  "remoteUser": "${localEnv:USER}",
10  "updateContainerUserID": true
11}

Dockerfile :

 1#FROM ubuntu:24.04
 2
 3ARG USERNAME
 4
 5# S'assurez-vous que toutes les commandes futures s'exécutent en tant que root 
 6# (nécessaire pour la création d'utilisateurs)
 7USER root
 8
 9# Supprimer les utilisateurs réguliers existants si présents
10RUN getent passwd \
11      | awk -F: '($3 >= 1000) && ($1 != "nobody") {print $1}' \
12      | xargs -r -n 1 userdel -r
13
14# Ajouter l'utilisateur hôte
15RUN if [ "${USERNAME}" != "root" ]; then \
16      groupadd --gid 1000 ${USERNAME} || true \
17      && useradd -s /bin/bash -m -u 1000 -g 1000 ${USERNAME} \
18      && mkdir /etc/sudoers.d/ \
19      && echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} \
20      && chmod 0440 /etc/sudoers.d/${USERNAME} \
21  fi

Voici une représentation visuelle de la structure du répertoire :

project-root/
└── .devcontainer/
    ├── devcontainer.json
    └── features
        └── user-sync
            ├── devcontainer-feature.json
            └── install.sh

Créer une Feature locale :

./.devcontainer/features/user-sync/devcontainer-feature.json :

 1{
 2  "name": "user-sync",
 3  "id": "user-sync",
 4  "version": "1.0.0",
 5  "options": {
 6    "username": {
 7      "type": "string",
 8      "description": "Nom de l'utilisateur à créer",
 9      "default": "devuser"
10    }
11  },
12  // Il doit s'exécuter après les common utils (qui créent des utilisateurs)
13  "installsAfter": ["ghcr.io/devcontainers/features/common-utils"]
14}

./.devcontainer/features/user-sync/install.sh :

 1#!/bin/bash
 2set -e
 3
 4# Supprimer les utilisateurs réguliers existants si présents
 5getent passwd \
 6  | awk -F: '($3 >= 1000) && ($1 != "nobody") {print $1}' \
 7  | xargs -r -n 1 userdel -r
 8    
 9# Ajouter l'utilisateur hôte (gracieusement sauter si root)
10if [ "${USERNAME}" != "root" ]; then 
11  groupadd --gid 1000 ${USERNAME} || true 
12  useradd -s /bin/bash -m -u 1000 -g 1000 ${USERNAME} 
13  mkdir /etc/sudoers.d/ 
14  echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} 
15  chmod 0440 /etc/sudoers.d/${USERNAME}
16fi

devcontainer.json :

 1{
 2  //"image": "ubuntu:24.04",
 3  "features": {
 4    "./features/user-sync": {
 5      // Passer le nom d'utilisateur de l'hôte en option de fonctionnalité
 6      "username": "${localEnv:USER}"
 7    }
 8  },
 9  "remoteUser": "${localEnv:USER}",
10  "updateContainerUserID": true
11}

Un seul utilisateur régulier + updateContainerUserID = synchro propre.

Quand la synchro complète n’est pas nécessaire

Parfois, vous voulez juste l’alignement UID/GID, pas le nom.

Voyons comment cela s’applique selon le type d’image.

1. Images DevContainer préconfigurées (mcr.microsoft.com/devcontainers)

Exemple : mcr.microsoft.com/devcontainers/base:ubuntu-22.04

Ces images contiennent déjà un utilisateur régulier, souvent vscode.

Donc :

1{
2  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04"
3}

Et tout fonctionne.

2. Images non préconfigurées

A. L’image contient déjà un seul utilisateur régulier

Exemple : l’image contient un seul utilisateur ubuntu :

1{
2  "image": "ubuntu:24.04",
3  "remoteUser": "ubuntu"
4}

B. L’image contient plusieurs utilisateurs réguliers ou aucun

Si l’image contient plusieurs utilisateurs réguliers, il existe un risque de conflit de UID. Vous devez donc réduire le nombre d’utilisateurs réguliers à un seul. Cela nécessite de modifier le Dockerfile ou le processus de build de l’image.

Si l’image ne contient aucun utilisateur régulier, vous devez en créer un dans l’image. C’est fréquent avec les images minimales ou basées sur des distributions, et cela nécessite également de modifier le processus de build.

Dans les deux cas, puisque l’image doit être modifiée, il est recommandé d’utiliser l’approche universelle privilégiée.

💡 Raison : Une reconstruction de l’image est de toute façon nécessaire. Le script universel gère à la fois la création d’un utilisateur et la suppression des utilisateurs supplémentaires. Cela garantit une configuration cohérente, quel que soit le type d’image de base utilisé.

Conclusion

La synchronisation utilisateur dans les DevContainers semble simple, mais elle casse vite dès que :

  • plusieurs utilisateurs existent
  • les UID se chevauchent
  • root est utilisé par défaut
  • les montages interfèrent
  • les dotfiles ne correspondent plus

La solution est toujours la même :

Assurez-vous qu’il n’y a qu’un seul utilisateur régulier dans l’image.
Optionnel : lui donner le même nom que l’hôte.
Puis laissez DevContainers ajuster UID/GID.

Avec la méthode Dockerfile ou Feature, vous éliminez définitivement les problèmes de permissions — quelle que soit l’image Linux utilisée.

Points d’attention importants

1. updateContainerUserID ne modifie qu’un seul utilisateur :
Celui défini dans remoteUser / containerUser. S’il y a collision UID, la mise à jour échoue.
2. Root n’est jamais modifié :
UID 0 reste UID 0. Ne développez jamais en root.
3. Ne modifiez pas les utilisateurs système :
Ils sont liés aux services internes. Ne touchez qu’à l’utilisateur régulier de développement.
4. remoteUser doit déjà exister :
Les DevContainers ne créent pas d’utilisateur automatiquement. Il doit être présent dans l’image avec un vrai home.

Ces points renforcent la règle clé :

Un seul utilisateur régulier → synchronisation fiable, portable et prévisible.