Files
qwen3-6-lora/data/raw/sanitized/plans/quiero-hacer-entrypoint-sh-user-drifting-blum.md

11 KiB

Hacer entrypoint.sh agnóstico de UID/GID (soporte HOST_UID/HOST_GID)

Fix post-implementación: resync espurio con GID 0

Síntoma observado: al correr el contenedor aparece, repetido muchas veces, groupmod: GID '0' already exists, además de la advertencia normal de adduser sobre que el home dir ya existe.

Causa raíz: entrypoint.sh se re-ejecuta más de una vez en el ciclo de arranque (el propio script hace exec sudo -u $HOME_USER bash -c "... /usr/bin/entrypoint.sh" para re-lanzarse como el usuario destino, y además el contenedor puede reiniciar el entrypoint). En cada ejecución se vuelve a calcular DETECTED_UID/DETECTED_GID con stat sobre /home/${HOME_USER}. En este entorno (bind mount vía WSL2/Docker Desktop), el stat del volumen reporta GID 0 (root) — es un artefacto de metadata del mount, no el GID real del usuario del host. Como la rama de "usuario ya existe" recalcula TARGET_GID de la misma manera que la de creación, en cada re-arranque posterior a la creación intenta groupmod -g 0 aleleba, lo cual falla siempre porque el GID 0 ya pertenece a root.

Fix acordado con el usuario: para un usuario que ya existe, solo se resincroniza UID/GID si HOST_UID/HOST_GID vienen seteados explícitamente por variable de entorno. La auto-detección vía stat del volumen montado se usa únicamente en la rama de creación de un usuario nuevo (para heredar la propiedad de un home pre-existente), no en cada reinicio de un usuario que ya existe. Además, se descarta cualquier UID/GID detectado que sea 0 (root) como no confiable, cayendo al default 1000 en ese caso, tanto en creación como al calcular el target explícito.

Esto preserva el caso de uso principal (HOST_UID=502 HOST_GID=502 fija el usuario a 502:502 tanto en la creación como en reinicios posteriores, sin error) y elimina el crash cuando no se pasa override y el mount reporta metadata espuria.

Cambios concretos sobre el bloque ya implementado

  1. Guardar contra UID/GID 0 al calcular DETECTED_UID/DETECTED_GID (cerca de la línea 10-13 actual de entrypoint.sh):

    DETECTED_UID=$(stat -c '%u' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
    DETECTED_GID=$(stat -c '%g' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
    [ "$DETECTED_UID" = "0" ] && DETECTED_UID=1000
    [ "$DETECTED_GID" = "0" ] && DETECTED_GID=1000
    TARGET_UID="${HOST_UID:-$DETECTED_UID}"
    TARGET_GID="${HOST_GID:-$DETECTED_GID}"
    

    (Si HOST_UID/HOST_GID se pasan explícitos, siguen ganando siempre, incluso si son 0 — esa sería una elección deliberada del operador, no un artefacto de mount.)

  2. En la rama else (usuario ya existe) del bloque en entrypoint.sh (~línea 88 actual), solo intentar el resync si hay override explícito:

    else
      # Usuario ya existe (imagen/volumen reusado) — resincronizar UID/GID
      # SOLO si el operador lo pidió explícitamente vía HOST_UID/HOST_GID.
      # No usar la detección automática del mount acá: en reinicios repetidos
      # es una señal poco confiable (ver metadata GID 0 de bind mounts en
      # WSL2/Docker Desktop) y causaba un groupmod fallido en cada arranque.
      if [[ -n "${HOST_UID-}" || -n "${HOST_GID-}" ]]; then
        CURRENT_UID=$(id -u "${HOME_USER}")
        CURRENT_GID=$(id -g "${HOME_USER}")
        CURRENT_GROUP=$(id -gn "${HOME_USER}")
    
        if [ "$CURRENT_UID" != "$TARGET_UID" ] || [ "$CURRENT_GID" != "$TARGET_GID" ]; then
          if [ "$CURRENT_GID" != "$TARGET_GID" ]; then
            sudo groupmod -g "${TARGET_GID}" "${CURRENT_GROUP}"
          fi
          if [ "$CURRENT_UID" != "$TARGET_UID" ]; then
            sudo usermod -u "${TARGET_UID}" "${HOME_USER}"
          fi
          sudo find / -xdev \( -user "${CURRENT_UID}" -o -group "${CURRENT_GID}" \) \
            -exec chown -h "${TARGET_UID}:${TARGET_GID}" {} + 2>/dev/null || true
        fi
      fi
    fi
    
  3. Actualizar la sección de readme.md sobre HOST_UID/HOST_GID para aclarar que, para un usuario ya existente (contenedor reiniciado/reusado), el resync solo ocurre si se pasan HOST_UID/HOST_GID explícitos — la auto-detección desde el volumen montado solo aplica la primera vez que se crea el usuario.

Verificación del fix

  1. bash -n entrypoint.sh.
  2. Reiniciar el contenedor sin HOST_UID/HOST_GID sobre un volumen ya existente (el mismo que dio el error) → no debe aparecer más groupmod: GID '0' already exists, ni ningún intento de resync.
  3. Reiniciar el contenedor con -e HOST_UID=502 -e HOST_GID=502 sobre un usuario ya creado con otro UID/GID → debe correr usermod/groupmod una vez y quedar en 502:502, sin error.
  4. Repetir el arranque una tercera vez con el mismo HOST_UID=502 HOST_GID=502 → no debe intentar usermod/groupmod de nuevo (ya coincide), y no debe haber errores.

Contexto

Hoy entrypoint.sh crea el usuario HOME_USER (default vscode) siempre con --uid 1000 hardcodeado (línea 70). Esto rompe cuando el /home/${HOME_USER} se monta desde un volumen del host cuyo dueño real tiene otro UID/GID (por ejemplo un usuario Linux del host distinto de 1000), generando problemas de permisos entre el contenedor y los archivos montados.

El objetivo es que el UID/GID del usuario dentro del contenedor:

  1. Se pueda fijar explícitamente vía HOST_UID / HOST_GID.
  2. Si no se fijan, se detecte automáticamente a partir del dueño real de /home/${HOME_USER} (cuando ya existe por venir de un volumen montado).
  3. Si tampoco se puede detectar, caiga al comportamiento actual (UID/GID 1000), preservando compatibilidad con quien no use esta variable.
  4. Si el contenedor se reinicia/reusa y el usuario del sistema ya existía con otro UID/GID, se resincronice (usermod -u, groupmod -g) y se re-asignen los archivos que quedaron con el UID/GID viejo, para no dejar huérfanos.

Cambio 1 — detección de UID/GID (nueva, antes de tocar /home)

Insertar justo después de resolver HOME_USER (después de la línea 6 actual, antes de que cualquier otra parte del script toque /home/${HOME_USER}).

Es importante hacerlo ahí y no más abajo: el loop de USER_ENV_ (líneas ~48-56 actuales) puede crear /home/${HOME_USER} con dueño root:root antes de llegar al bloque de creación de usuario, si hay variables USER_ENV_* seteadas y el directorio no viene de un bind mount. Si la detección corriera después de ese loop, leería root:root en vez de caer al default 1000.

DETECTED_UID=$(stat -c '%u' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
DETECTED_GID=$(stat -c '%g' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
TARGET_UID="${HOST_UID:-$DETECTED_UID}"
TARGET_GID="${HOST_GID:-$DETECTED_GID}"

Compatible con set -eu: ${VAR:-default} no dispara set -u, y el || echo 1000 garantiza que la sustitución de stat nunca falle bajo set -e.

Cambio 2 — reemplazar el bloque de creación de usuario (líneas 68-80 actuales)

USER="$HOME_USER"
if ! id -u "$HOME_USER" > /dev/null 2>&1; then
  sudo groupadd -g "${TARGET_GID}" "${HOME_USER}" 2>/dev/null || true
  sudo adduser --disabled-password --gecos "" --uid "${TARGET_UID}" --gid "${TARGET_GID}" "${HOME_USER}"
  sudo echo "$HOME_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null

  # Creating .vscode folder if it doesn't exist
  if [ ! -d "/home/${HOME_USER}/.vscode" ]; then
    sudo mkdir -p "/home/${HOME_USER}/.vscode"
  fi

  # Changing the property of the directory /home/${HOME_USER}/.vscode
  sudo chown -R "${HOME_USER}" "/home/${HOME_USER}/.vscode"
else
  # Usuario ya existe (imagen/volumen reusado) — resincronizar UID/GID si cambiaron
  CURRENT_UID=$(id -u "${HOME_USER}")
  CURRENT_GID=$(id -g "${HOME_USER}")
  CURRENT_GROUP=$(id -gn "${HOME_USER}")

  if [ "$CURRENT_UID" != "$TARGET_UID" ] || [ "$CURRENT_GID" != "$TARGET_GID" ]; then
    if [ "$CURRENT_GID" != "$TARGET_GID" ]; then
      sudo groupmod -g "${TARGET_GID}" "${CURRENT_GROUP}"
    fi
    if [ "$CURRENT_UID" != "$TARGET_UID" ]; then
      sudo usermod -u "${TARGET_UID}" "${HOME_USER}"
    fi
    sudo find / -xdev \( -user "${CURRENT_UID}" -o -group "${CURRENT_GID}" \) \
      -exec chown -h "${TARGET_UID}:${TARGET_GID}" {} + 2>/dev/null || true
  fi
fi

Notas de diseño:

  • groupadd -g "${TARGET_GID}" "${HOME_USER}" corre siempre (con || true) antes del adduser, porque adduser --gid <numérico> requiere que ya exista un grupo con ese GID.
  • A diferencia del borrador original (que solo comparaba UID), también se compara GID y se resincroniza con groupmod si cambió — si no, un volumen cuyo GID cambió quedaría con archivos "group-orphan".
  • id -gn resuelve el nombre real del grupo primario actual (por si no coincide con ${HOME_USER}), en vez de asumirlo.
  • El resync de archivos hace un solo find con -user X -o -group Y y chown -h uid:gid, cubriendo tanto el caso de UID como de GID en un solo pase.
  • Se mantiene sin cambios el sudo find "/home/${HOME_USER}" -xdev -exec chown "${HOME_USER}" {} + que ya existe más abajo (línea ~86, en la rama donde el script ya corre como el usuario objetivo) — no es redundante, es una red de seguridad adicional sobre /home, y sigue siendo válida tanto si el usuario se creó de cero como si se resincronizó.

Riesgos conocidos (documentar, no bloquean el cambio)

  • Si TARGET_UID/TARGET_GID ya están tomados por otra cuenta/grupo del sistema, usermod -u / groupmod -g fallarán (comportamiento actual de esas herramientas; no se agrega manejo especial).
  • El find / -xdev de resincronización recorre todo el filesystem del contenedor (limitado por -xdev a no cruzar de montaje) — tiene un costo de arranque en imágenes con muchos archivos, pero es el mismo patrón que ya usa el script en la línea 86.

Cambio 3 — actualizar readme.md para documentar las nuevas env vars

  • En la sección "Environment Variables" (líneas 13-18), agregar HOST_UID y HOST_GID junto a HOME_USER/VSCODE_TUNNEL_NAME, explicando que son opcionales y que si no se setean se autodetectan desde el volumen montado o caen a 1000.
  • En la sección "Using this image as a base image in a Dockerfile" (líneas 148-172), agregar una nota breve indicando que el UID 1000 de ese ejemplo es solo ilustrativo y que en runtime entrypoint.sh puede resincronizarlo vía HOST_UID/HOST_GID si se pasan al contenedor final.

No se toca el Dockerfile (el fixuid hardcodeado a vscode en la línea 22 es código muerto hoy — no se invoca en entrypoint.sh — y está fuera del alcance de este cambio).

Verificación

  1. bash -n entrypoint.sh para chequeo de sintaxis.
  2. Levantar el contenedor sin HOST_UID/HOST_GID y sin volumen montado en /home/vscode → debe crear el usuario con UID/GID 1000 (comportamiento actual preservado).
  3. Levantar el contenedor con -e HOST_UID=1500 -e HOST_GID=1500 → el usuario creado debe tener ese UID/GID (id vscode dentro del contenedor).
  4. Montar un volumen en /home/vscode cuyo contenido pertenezca a un UID/GID distinto de 1000 (sin pasar HOST_UID/HOST_GID) → el usuario creado debe tomar ese UID/GID detectado (stat -c '%u:%g' /home/vscode).
  5. Simular "imagen reusada": crear el usuario una vez, luego reiniciar el contenedor con un HOST_UID/HOST_GID distinto → verificar que usermod/groupmod corrieron y que archivos de prueba en /home/vscode quedaron con el nuevo dueño (ls -n /home/vscode).