Docker: permisos UID/GID — La solución de volúmenes que acaba con ‘Permission denied’

¿Te fue útil?

Hiciste lo responsable: montaste un volumen para que tus datos sobrevivan los reinicios del contenedor.
Luego tu aplicación arranca y se estrella con Permission denied como si acabara de conocer Linux por primera vez.

Esto no es un momento de “Docker está defectuoso”. Es Linux haciendo exactamente lo que pediste—solo que no lo que querías.
Los contenedores no tienen “usuarios”. Tienen números. Y tu volumen también está propiedad de números.
Cuando esos números no coinciden, obtienes el clásico mensaje de producción: “no”.

El problema real: los usuarios son números, los volúmenes son sistemas de archivos

Docker no inventó los permisos. Los heredó. Un proceso dentro del contenedor se ejecuta con un UID y uno o más GIDs,
igual que cualquier otro proceso en Linux. Un volumen (bind mount o volumen nombrado) está respaldado por un sistema de archivos real con
propiedad de inodos real (UID/GID) y bits de modo reales (rwx). Docker simplemente conecta los dos.

Cuando una imagen dice “ejecutar como appuser,” lo que realmente significa en tiempo de ejecución es “ejecutar como UID 1001 (o 999, o 70, o el que haya elegido quien construyó la imagen).”
Mientras tanto, tu directorio en el host puede estar en propiedad de UID 1000. O root. O de un UID respaldado por LDAP que no existe dentro del contenedor.
A Linux no le importan los nombres. Compara enteros. Si no coinciden y no hay permisos de grupo/ACL, el acceso falla.

Volumen nombrado vs bind mount: la trampa de permisos cambia de forma

Dos estilos comunes de montaje, dos modos de fallo ligeramente diferentes:

  • Bind mount (-v /host/path:/container/path): el contenedor ve el directorio del host exactamente, incluyendo propiedad y ACLs.
    Excelente para depuración; brutal para la deriva de “funciona en mi máquina”.
  • Volumen nombrado (-v myvol:/container/path): Docker crea y gestiona un directorio bajo su raíz de datos.
    La propiedad sigue siendo basada en UID/GID, pero ahora lo crea Docker/daemon con valores por defecto y a veces “ayudado” por entrypoints de la imagen.

La verdad incómoda: chmod 777 no es una solución

Si alguna vez “resolviste” esto con chmod -R 777, no arreglaste permisos. Declaraste bancarrota.
Usualmente funciona, y generalmente vuelve más tarde como un incidente de seguridad, un hallazgo de cumplimiento o una historia de corrupción de datos misteriosa.

El enfoque correcto es aburrido: hacer que el UID/GID en tiempo de ejecución del contenedor coincida con la propiedad del volumen (o viceversa), de forma intencionada y repetible.

Una cita que vale la pena pegar en una nota:
“La esperanza no es una estrategia.” — Gene Kranz

Depurar permisos es donde la esperanza va a morir.

Guía de diagnóstico rápido

Cuando estás de guardia, no necesitas una conferencia. Necesitas una ruta rápida a la causa raíz.
Aquí está el orden que suele terminar con el dolor rápidamente.

Primero: confirma con qué UID/GID se está ejecutando realmente el proceso

  • Comprueba el usuario en tiempo de ejecución del contenedor (id dentro del contenedor, o inspecciona User en la configuración).
  • Si es root y aún falla, sospecha de SELinux/AppArmor, root-squash de NFS o montajes de solo lectura.

Segundo: revisa el tipo de montaje y su propietario/modo en disco

  • ¿Es un bind mount o un volumen nombrado?
  • En el host, inspecciona la propiedad (stat) y cualquier ACL (getfacl).

Tercero: comprueba el sistema de archivos y las capas de seguridad

  • ¿SELinux en modo enforcing? ¿Falta el etiquetado :Z/:z en bind mounts?
  • ¿NFS/CIFS con root-squash o identidades mapeadas?
  • ¿Docker rootless o namespaces de usuario que desplazan IDs?

Luego: elige una estrategia de solución y hazla determinista

  • Mejor por defecto: ejecuta el contenedor con el UID/GID del host y pre-crea los directorios.
  • Alternativa: chown del volumen una vez (con cuidado) mediante un paso de init, no en cada arranque.
  • Evita: chown recursivo perpetuo en entrypoints para datasets grandes.

Broma #1: El chown recursivo es lo más parecido que tiene Linux a una app de meditación. Te obliga a sentarte y pensar en tus opciones.

La corrección de volúmenes que realmente lo termina

La solución que sobrevive reconstrucciones, cambios de nodo y la creatividad humana es simple:
alinear UID/GID entre el proceso del contenedor y la propiedad del volumen.
Hazlo explícito, no “lo que hizo el autor de la imagen”.

El patrón más fiable para Docker Compose

Cuando controlas el directorio del host y usas bind mounts, establece user en Compose al UID/GID del usuario del host,
y haz que el directorio del host pertenezca a ese UID/GID.

cr0x@server:~$ id
uid=1000(cr0x) gid=1000(cr0x) groups=1000(cr0x),27(sudo),998(docker)

Decisión: si tu servicio debe escribir en un directorio montado por bind propiedad de tu usuario de despliegue (UID 1000),
ejecuta el contenedor como 1000:1000. No estás “volviéndolo menos seguro.” Lo estás haciendo predecible.

Ejemplo de fragmento Compose (conceptual; implementa en tu stack):

  • Host: /srv/myapp/data propiedad de 1000:1000, modo 0750 o más estricto.
  • Contenedor: el proceso se ejecuta como 1000:1000.

Si debes usar un volumen nombrado

Los volúmenes nombrados están bien. También son lo suficientemente opacos como para que los equipos empiecen a adivinar.
El enfoque sensato es:

  1. Crea el volumen.
  2. Inicializa la propiedad una vez, en un paso controlado y auditable.
  3. Ejecuta la app real como un UID no root que coincida con esa propiedad.

El antipatrón es dejar que el contenedor principal se ejecute como root solo para “arreglar permisos” en el arranque.
Así es como terminas con un servicio que es root para siempre porque alguien tiene miedo de tocarlo.

¿Y “solo chown”?

Chown puede ser correcto. También puede ser catastrófico.
En volúmenes grandes, chown recursivo es un recorrido completo del sistema de archivos; en almacenamiento en red, es una negación de servicio en cámara lenta.
El truco es hacerlo una vez, y solo si estás seguro de que apuntas a la ruta correcta.

Patrones que funcionan (y por qué)

Patrón A: Ejecutar el contenedor como el UID/GID del host (mejor por defecto para bind mounts)

Este es el enfoque de “hacer feliz a Linux”. El sistema de archivos ya tiene propiedad. Haz que el proceso coincida.
Evita tormentas de chown y funciona bien cuando tu proceso de despliegue ya tiene una cuenta de servicio estable.

Requiere que la aplicación pueda ejecutarse como no root. La mayoría de las imágenes modernas pueden.
Si una imagen insiste en root sin buena razón, considéralo un olor a problema.

Patrón B: Inicializar la propiedad del volumen una vez (mejor por defecto para volúmenes nombrados)

Creas un pequeño contenedor de un solo uso cuya única tarea es crear directorios y establecer propiedad/ACLs,
luego ejecutas la app como un usuario normal sin privilegios.

Bien hecho, es determinista. Mal hecho, es una trampa con pasos extra. La diferencia es:
apuntas rutas exactas, evitas chown recursivos a menos que sean necesarios y registras lo que hiciste.

Patrón C: ACLs en lugar de propiedad (genial cuando múltiples UIDs necesitan escribir)

A veces tienes sidecars o varios contenedores escribiendo al mismo montaje (agente de backups, log shipper, app).
La propiedad no puede satisfacer a todos a menos que obligues a compartir un UID (lo que se vuelve un lío).
Las ACLs te permiten dar acceso de escritura a UIDs/GIDs adicionales sin volver el directorio escribible para todo el mundo.

Patrón D: Uso de propiedad de grupo + directorios setgid (Unix clásico, aún útil)

Si múltiples procesos necesitan crear archivos en el mismo directorio, hazlo escribible por grupo y pon el bit setgid en el directorio.
Los archivos nuevos heredan el grupo del directorio. Es una de esas características Unix antiguas que silenciosamente resuelven problemas reales.

Patrón E: Docker rootless y namespaces de usuario (seguro, pero cambia las reglas)

Docker rootless y userns-remap son excelentes opciones de seguridad. También reescriben el mapeo UID/GID.
Tu UID 0 dentro del contenedor puede mapear a un UID del host 100000+.
Eso significa que un directorio del host propiedad de UID 0 no es escribible por el “root” en el contenedor—porque no es realmente root del host.

Esto no es un bug. Es el objetivo.

Broma #2: “Solo ejecútalo como root” es el equivalente en contenedores de “simplemente reinicia prod.” Funciona, y también te delata.

Tareas prácticas: comandos, salidas, decisiones

A continuación hay tareas reales que puedes ejecutar durante la respuesta a incidentes o en el endurecimiento preventivo.
Cada una incluye: el comando, qué significa la salida y qué decisión tomar.

Tarea 1: Identificar el UID/GID efectivo del contenedor

cr0x@server:~$ docker exec -it myapp sh -lc 'id && umask'
uid=1001(app) gid=1001(app) groups=1001(app)
0022

Significado: el proceso se ejecuta como UID/GID 1001 y crea archivos con umask por defecto 0022.
Decisión: el directorio montado debe ser escribible por UID 1001 (propietario o ACL) o por un grupo al que pertenezca el proceso.

Tarea 2: Inspeccionar qué usuario cree Docker que está ejecutando

cr0x@server:~$ docker inspect -f '{{.Config.User}}' myapp
1001:1001

Significado: el contenedor está configurado para ejecutarse como usuario/grupo numérico.
Decisión: iguala la propiedad del directorio del host a 1001:1001, o cambia el usuario en tiempo de ejecución para que coincida con el directorio.

Tarea 3: Confirmar la fuente del montaje y su tipo

cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}' myapp
bind /srv/myapp/data -> /var/lib/myapp
volume myapp-cache -> /cache

Significado: /var/lib/myapp es un bind mount; /cache es un volumen nombrado.
Decisión: diagnostica permisos del bind mount en la ruta del host; diagnostica volumen nombrado vía la ubicación del volumen de Docker.

Tarea 4: Comprobar la propiedad y el modo del directorio en el host

cr0x@server:~$ stat -c 'path=%n owner=%u:%g mode=%a type=%F' /srv/myapp/data
path=/srv/myapp/data owner=0:0 mode=755 type=directory

Significado: propiedad de root, no escribible por otros (755 significa que solo el propietario puede escribir).
Decisión: o chown a 1001:1001, o ejecutar el contenedor como root (no recomendado), o usar escritura por grupo/ACL.

Tarea 5: Simular acceso usando un contenedor desechable como un UID específico

cr0x@server:~$ docker run --rm -u 1001:1001 -v /srv/myapp/data:/mnt alpine sh -lc 'touch /mnt/test && ls -ln /mnt/test'
touch: /mnt/test: Permission denied

Significado: UID 1001 no puede escribir en el bind mount; la falla es reproducible fuera de tu app.
Decisión: arregla primero los permisos del sistema de archivos; no pierdas tiempo “depurando la app.”

Tarea 6: Arreglar la propiedad del bind mount (dirigido, no recursivo salvo que sea necesario)

cr0x@server:~$ sudo chown 1001:1001 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=755

Significado: el propietario del directorio ahora es UID/GID 1001. Modo 755 significa que el propietario puede escribir.
Decisión: vuelve a probar el acceso de escritura. Si la app necesita colaboración por grupo, ajusta modo/ACL según corresponda.

Tarea 7: Usar setgid + escritura por grupo para directorios compartidos

cr0x@server:~$ sudo chgrp 1001 /srv/myapp/data
cr0x@server:~$ sudo chmod 2775 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=2775

Significado: el bit setgid está activado (2xxx). Los archivos nuevos heredan el grupo 1001; el grupo tiene escritura.
Decisión: usa esto cuando múltiples procesos comparten un GID y quieres propiedad de grupo predecible.

Tarea 8: Añadir una ACL para un segundo UID escritor sin aflojar los bits de modo

cr0x@server:~$ sudo setfacl -m u:1002:rwx /srv/myapp/data
cr0x@server:~$ getfacl -p /srv/myapp/data | sed -n '1,12p'
# file: /srv/myapp/data
# owner: 1001
# group: 1001
user::rwx
user:1002:rwx
group::rwx
mask::rwx
other::r-x

Significado: UID 1002 tiene explícitamente rwx en el directorio; la máscara lo permite.
Decisión: elige ACLs cuando tienes múltiples UIDs en contenedores y no quieres que “todos sean 1000”.

Tarea 9: Localizar un volumen nombrado en el host (para depuración e inicialización)

cr0x@server:~$ docker volume inspect myapp-cache -f '{{.Mountpoint}}'
/var/lib/docker/volumes/myapp-cache/_data

Significado: el volumen nombrado vive bajo la raíz de datos de Docker.
Decisión: usa esta ruta para inspección a nivel de host (stat, getfacl) o para una configuración cautelosa y única de permisos.

Tarea 10: Inspeccionar la propiedad del volumen nombrado y confirmar desajuste de UID del contenedor

cr0x@server:~$ sudo stat -c 'owner=%u:%g mode=%a' /var/lib/docker/volumes/myapp-cache/_data
owner=0:0 mode=755

Significado: directorio del volumen propiedad de root; el usuario del contenedor 1001 no puede escribir.
Decisión: inicializa la propiedad una vez (chown dirigido de lo que necesites), luego ejecuta la app sin privilegios.

Tarea 11: Realizar un init de una sola vez para chown de un volumen de forma segura (alcance limitado)

cr0x@server:~$ docker run --rm -v myapp-cache:/cache alpine sh -lc 'addgroup -g 1001 app && adduser -D -u 1001 -G app app; mkdir -p /cache/app; chown -R 1001:1001 /cache/app; ls -ldn /cache/app'
drwxr-xr-x    2 1001     1001          4096 Feb  4 12:00 /cache/app

Significado: se creó un subdirectorio /cache/app propiedad de 1001:1001, sin chown recursivo de toda la raíz del volumen.
Decisión: monta y usa ese subdirectorio desde la app para evitar peleas inesperadas por la propiedad.

Tarea 12: Validar dentro del contenedor de la app que la escritura funciona ahora

cr0x@server:~$ docker exec -it myapp sh -lc 'touch /cache/app/ok && ls -ln /cache/app/ok'
-rw-r--r--    1 1001     1001            0 Feb  4 12:01 /cache/app/ok

Significado: archivo creado con la propiedad numérica correcta.
Decisión: has terminado—a menos que SELinux/AppArmor o semánticas NFS estén involucradas.

Tarea 13: Comprobar montajes de solo lectura (sorprendentemente comunes)

cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{if .RW}}{{else}}{{println "RO:" .Destination}}{{end}}{{end}}' myapp
RO: /var/lib/myapp

Significado: el montaje es de solo lectura a nivel de Docker.
Decisión: corrige primero las banderas de Compose/run; los permisos no importan si el montaje es RO.

Tarea 14: Comprobación SELinux (bind mounts que deberían funcionar, pero no lo hacen)

cr0x@server:~$ getenforce
Enforcing

Significado: SELinux está en modo enforcing.
Decisión: si los bind mounts fallan pese a UID/GID/mode correctos, etiqueta el bind mount apropiadamente (p. ej., con :Z/:z) y vuelve a probar.

Tarea 15: Detectar comportamiento NFS root-squash (root no puede arreglar lo que root no puede poseer)

cr0x@server:~$ mount | grep ' /srv/myapp '
nfs01:/exports/myapp on /srv/myapp type nfs4 (rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.10,local_lock=none,addr=10.0.0.20)

Significado: el almacenamiento de respaldo es NFSv4. Root-squash se configura del lado del servidor, no es visible aquí.
Decisión: si chown falla con “Operation not permitted,” deja de pelear con el contenedor y arregla el mapeo de identidades/opciones de exportación.

Tarea 16: Comprobar remapeo de namespaces de usuario (los UIDs no coincidirán con lo que crees)

cr0x@server:~$ docker info | sed -n '1,120p' | grep -E 'rootless|userns'
rootless: false
userns: host

Significado: sin remapeo userns; los UIDs del contenedor mapean directamente a UIDs del host.
Decisión: la alineación UID/GID es directa. Si userns está activado, incorpora los rangos de remapeo en tu plan.

Tarea 17: Confirmar que el kernel ve el montaje donde crees que está

cr0x@server:~$ docker exec -it myapp sh -lc 'grep -E " /var/lib/myapp | /cache " /proc/mounts'
/dev/sda1 /var/lib/myapp ext4 rw,relatime 0 0
/dev/sda1 /cache ext4 rw,relatime 0 0

Significado: los montajes están presentes y rw a nivel de kernel.
Decisión: si las escrituras siguen fallando, es permisos, SELinux/AppArmor, atributos inmutables o semánticas de sistemas de archivos en red.

Tarea 18: Comprobar atributo inmutable (raro, pero sucede)

cr0x@server:~$ sudo lsattr -d /srv/myapp/data
-------------------P-- /srv/myapp/data

Significado: no hay atributo inmutable (i) activado.
Decisión: si ves ----i--------, quítalo con chattr -i (con cuidado) antes de perseguir otros fantasmas.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: “Permission denied” en el arranque, pero solo en un host

Causa raíz: el bind mount apunta a un directorio con propiedad/ACLs diferentes en ese host (a menudo creado manualmente o por una ejecución de automatización distinta).

Solución: estandariza la creación de directorios en la provisión. Haz cumplir la propiedad/modo vía gestión de configuración. Ejecuta contenedores con UID/GID numéricos explícitos.

2) Síntoma: el contenedor se ejecuta como root y aún así no puede escribir

Causa raíz: desajuste de etiqueta SELinux, montaje de solo lectura, o NFS root-squash.

Solución: comprueba la bandera RW del montaje; comprueba getenforce; si es NFS, arregla el mapeo de identidad del export; si es SELinux, etiqueta el montaje apropiadamente.

3) Síntoma: “Operation not permitted” al intentar chown un directorio montado

Causa raíz: el sistema de archivos no soporta chown como esperas (común en NFS/CIFS), o no tienes privilegios para cambiar la propiedad allí.

Solución: alinea identidades en la capa de almacenamiento (mapeo UID), o usa ACLs soportadas por ese sistema de archivos, o escribe en un directorio ya propiedad del usuario correcto.

4) Síntoma: funciona después de borrar el volumen, luego vuelve a fallar

Causa raíz: “lo arreglaste” reiniciando el estado. La siguiente ejecución recrea directorios con propiedad root o un UID diferente.

Solución: añade un paso de inicialización determinista (crear subdirectorio + chown una vez) y deja de confiar en valores por defecto implícitos.

5) Síntoma: archivos creados por la app son propiedad de root en el host

Causa raíz: el proceso del contenedor se ejecuta como root, o el entrypoint de la imagen cambia usuarios de forma incorrecta, o usaste user: "0" como arreglo rápido.

Solución: ejecuta como UID numérico no root; asegúrate de que el entrypoint no se vuelva a escalar; verifica con id dentro del contenedor.

6) Síntoma: el sidecar de logs/backup no puede leer archivos creados por la app

Causa raíz: umask demasiado restrictiva, sin estrategia de grupo compartido, o sin ACL para el segundo UID.

Solución: usa propiedad de grupo + setgid en directorios; o añade una ACL; o alinea ambos contenedores a un GID compartido.

7) Síntoma: tras habilitar Docker rootless, todo es “Permission denied”

Causa raíz: las rutas del host son propiedad del verdadero root, pero el “root” rootless del contenedor se mapea a un rango de UIDs no privilegiado del host.

Solución: chown los directorios del host al usuario rootless (o ajusta subuid/subgid), o evita bind mounts que requieran propiedad root del host.

8) Síntoma: retraso masivo en el arranque cuando el contenedor inicia

Causa raíz: el entrypoint hace chown recursivo en un dataset montado grande.

Solución: elimina chown recursivo del camino crítico; haz un init de una sola vez; apunta a un subdirectorio; o usa aprovisionamiento a nivel de sistema de archivos.

9) Síntoma: los permisos parecen correctos, pero las escrituras fallan solo en producción

Causa raíz: producción usa SELinux en enforcing o un backend de almacenamiento diferente (NFS/CephFS) con semánticas distintas.

Solución: prueba con configuraciones de seguridad tipo producción; maneja explícitamente etiquetas SELinux y el mapeo de identidades en sistemas de archivos en red.

Tres microhistorias corporativas desde el terreno

Microhistoria #1: El incidente causado por una suposición equivocada

Un equipo desplegó un job runner conteinerizado que escribía artefactos en un directorio bind-mounted bajo /srv/builds.
En staging iba bien. En producción, la creación de artefactos empezó a fallar con Permission denied de forma aleatoria.
El on-call revisó los sospechosos habituales: fallo de almacenamiento, disco lleno, imagen corrupta.

La suposición equivocada fue sutil: “el directorio del host siempre lo crea nuestra automatización, así que siempre pertenece a la cuenta de servicio.”
Salvo que un host de producción fue reconstruido apresuradamente durante una ventana de mantenimiento. Un humano recreó /srv/builds manualmente.
Root lo poseía, modo 755, y el contenedor se ejecutaba como UID 1001.

El patrón de fallo parecía aleatorio porque los jobs se programaban en varios hosts. Los que caían en el host “hecho a mano” fallaban.
Nadie notó la diferencia de propiedad hasta que alguien ejecutó stat en la flota y vio el caso discordante.

La solución no fue heroica. Añadieron una comprobación de preflight simple en la provisión que afirmaba la propiedad y el modo, y configuraron Compose para ejecutar el contenedor como 1001:1001 explícitamente.
Lo más importante: dejaron de confiar en nombres de usuario en la documentación—cada runbook empezó a usar UID/GID numéricos.

La lección: si la existencia de un directorio es parte de la corrección, entonces su propiedad también lo es. Linux no funciona con sensaciones.

Microhistoria #2: La optimización que salió mal

Otra organización tenía un servicio de procesamiento de datos grande. Para reducir trabajo operativo, añadieron un paso de arranque que hacía
chown -R app:app /data en cada arranque del contenedor. Hizo desaparecer tickets de soporte—por un tiempo.
También hizo que los despliegues fuesen “fiables”, en el sentido de que esperar dos horas es confiablemente lento.

Al principio fue solo una molestia. Luego el dataset creció. Una actualización rolling significó múltiples contenedores en paralelo, cada uno recorriendo recursivamente un árbol de terabytes.
El CPU se disparó, el I/O de metadatos se disparó, y el backend de almacenamiento empezó a lanzar alertas de latencia. La app estaba “saludable”, el clúster no.

El retroceso no fue solo rendimiento. El chown recursivo tocó timestamps y propiedad en archivos que usaban sistemas downstream para detectar “frescura”.
Un montón de pipelines reprocesaron datos porque la propiedad cambió, lo que cambió metadatos, lo que activó heurísticas de “datos nuevos”.
Nadie se divirtió.

La solución fue mover la inicialización de permisos fuera del contenedor en tiempo de ejecución:
un job de provisión único creó un subdirectorio dedicado y estableció la propiedad una vez.
También cambiaron la aplicación para que rehuse iniciar si no puede escribir, en lugar de intentar “reparar” permisos. Fallar rápido, fallar honesto.

La lección: “permisos autocurativos” suele ser simplemente “quemar lentamente tu sistema de archivos.”

Microhistoria #3: La práctica aburrida pero correcta que salvó el día

Un equipo de plataforma estandarizó una política: cada contenedor con estado debe declarar un user numérico en Compose/Kubernetes,
y cada montaje escribible debe ser aprovisionado con propiedad coincidente antes de programar la carga de trabajo.
Sin excepciones, sin “root temporal”, sin 777. Molestó a los desarrolladores por aproximadamente una semana.

Meses después, migraron una flota de servicios de discos locales a almacenamiento respaldado por NFS para simplificar backups.
NFS introdujo sus habituales rarezas de mapeo de identidad. Predeciblemente, algunos servicios empezaron a lanzar errores de permisos durante el primer canario.
Pero porque el UID/GID estaba declarado y era consistente, la superficie de depuración fue mínima: o era mapeo del export o aprovisionamiento del directorio.

El on-call no adivinó. Comprobó el UID declarado, comprobó la propiedad del directorio en el servidor NFS, arregló el mapeo y siguió adelante.
No hubo rebuilds de imagen. No hubo “ejecutar como root” de emergencia. No hubo chmod de medianoche.

La práctica fue aburrida: identidades numéricas explícitas, rutas escribibles preaprovisionadas y una comprobación de preflight en CI que validaba que el contenedor se ejecuta sin root.
Esa aburrición pagó la cuenta.

La lección: la estandarización vence a la astucia, especialmente en torno a permisos.

Hechos interesantes y contexto histórico

  • Los UIDs y GIDs existen desde antes de los contenedores. Docker no creó el modelo; se apoya en la propiedad Unix clásica y los bits de modo.
  • Los nombres son decoración. Linux almacena la propiedad como enteros en los inodos; los nombres de usuario se resuelven después vía /etc/passwd o NSS.
  • Las configuraciones tempranas de contenedores usaban root por defecto. Era conveniente, y educó a una generación a normalizar ejecutar servicios como UID 0.
  • Los namespaces de usuario cambiaron la definición de “root”. Con userns o modo rootless, el root del contenedor puede mapear a un rango de UIDs no privilegiados del host.
  • Las overlays no “arreglaron” los permisos. OverlayFS resuelve el layering; los volúmenes montados siguen obedeciendo la propiedad y la política del sistema de archivos de respaldo.
  • El NFS root-squash es anterior a la mayoría de plataformas de contenedores. Es una característica de seguridad del almacenamiento que hace que root en el cliente se comporte como “nobody”.
  • Las ACLs no son nuevas. Las ACLs POSIX llevan años; simplemente se usan poco porque los bits de modo son más fáciles de explicar.
  • El etiquetado SELinux es un eje separado de UID/GID. Puedes tener propiedad perfecta y aun así ser denegado por política MAC.
  • Muchas imágenes oficiales estandarizaron UIDs fijos. Las imágenes de bases de datos a menudo usan IDs numéricos estables para que los volúmenes sigan compatibles tras actualizaciones—cuando respetas esos IDs.

Listas de verificación / plan paso a paso

Plan paso a paso: hacer que un contenedor con estado deje de pelear con su volumen

  1. Elige la identidad en tiempo de ejecución.
    Decide el UID/GID numérico que el servicio debe usar. Usa un número dedicado y estable (no “lo que la imagen base eligió hoy”).
  2. Elige una estrategia de montaje.
    Bind mount para control explícito del host; volumen nombrado para portabilidad; almacenamiento en red solo si entiendes su modelo de identidades.
  3. Provisiona la ruta escribible.
    Crea el directorio (o un subdirectorio) y establece propiedad/modo. Prefiere chown dirigido sobre recursivo.
  4. Hazlo comprobable.
    Añade una comprobación de preflight: ¿puede el contenedor escribir un archivo temporal en el montaje al arrancar? Si no, falla rápido con una línea clara en el log.
  5. Mantén init separado de runtime.
    Si necesitas chown, hazlo como un job de init de una sola vez o como un paso operativo manual, no en cada arranque del contenedor.
  6. Maneja SELinux/AppArmor explícitamente.
    Si ejecutas SELinux en modo enforcing, incorpora el etiquetado de montajes en tu configuración de run/compose.
  7. Documenta IDs numéricos.
    Coloca UID/GID en el repositorio junto a los manifiestos de Compose. Los nombres cambian; los números no.
  8. Ensaya la ruta de recuperación ante desastres.
    Si restauras un volumen desde backup, ¿preserva la propiedad? Si no, ¿qué paso de re-propiedad se requiere?

Lista previa al despliegue (usa antes de la primera ejecución en producción)

  • El contenedor se ejecuta como no root (verifica con id dentro del contenedor).
  • Los destinos de montaje son escribibles por ese UID/GID (verifica con un touch desechable).
  • No hay chown recursivo en entrypoints para rutas de datos grandes.
  • Los sistemas con SELinux en enforcing tienen una estrategia correcta de etiquetado para bind mounts.
  • Los sistemas de archivos en red tienen un plan de mapeo de identidades (paridad UID o directorio con propiedad adecuada).
  • El proceso de backup/restore preserva o reaplica la propiedad de forma determinista.

Lista de incidentes (cuando “Permission denied” te despierta)

  • Confirma UID/GID del contenedor (docker exec ... id).
  • Confirma tipo de montaje y estado RW (docker inspect).
  • Comprueba propiedad/modo/ACL de la ruta host (stat, getfacl).
  • Comprueba SELinux enforcing (getenforce) y logs de auditoría si aplica.
  • Comprueba NFS/CIFS y síntomas de root-squash (mount, comportamiento de chown).
  • Aplica la solución más pequeña segura: alineación de propiedad, ACL o etiquetado correcto—no 777.

Preguntas frecuentes

1) ¿Por qué mi usuario de contenedor existe en la imagen pero no en el host?

Porque son espacios de nombres separados para los nombres de usuario. Solo los IDs numéricos importan para las comprobaciones de permisos del sistema de archivos.
El host no necesita conocer el nombre de usuario; necesita que UID/GID coincidan con la propiedad o los permisos otorgados.

2) Si ejecuto el contenedor con -u 1000:1000, ¿necesito ese usuario dentro de la imagen?

No estrictamente. El kernel aplica los IDs numéricos. Algunas aplicaciones esperan una entrada en passwd para el UID (para llamadas getpwuid()).
Si la app se queja, añade una entrada o usa una imagen que soporte UIDs arbitrarios.

3) ¿Es seguro chownear un directorio de volumen Docker bajo /var/lib/docker?

Puede serlo, pero sé disciplinado. Prefiere crear y poseer un subdirectorio dentro del volumen en lugar de cambiar la raíz del volumen.
Evita chown recursivos en datos grandes. Y nunca ejecutes comandos “a ciegas” como root en ese árbol durante un incidente.

4) ¿Por qué se rompen los permisos después de restaurar desde backup?

Muchas herramientas de backup preservan contenido pero no la propiedad numérica, o restauran como el usuario que ejecuta la restauración.
Si los archivos restaurados quedan en propiedad de root (o un UID distinto), tu usuario de contenedor pierde acceso de escritura.
Arregla restaurando con propiedad preservada o ejecutando un paso controlado de re-propiedad después.

5) ¿Cuál es la diferencia entre chmod y chown en este contexto?

chown cambia quién posee los archivos (qué UID/GID tiene los “derechos de propietario”).
chmod cambia qué pueden hacer propietario/grupo/otros. Si el propietario es incorrecto, chmod suele solo reorganizar la decepción.

6) Mi app se ejecuta como root en el contenedor. ¿No está bien porque está “aislada”?

El aislamiento no es absoluto. Root en el contenedor tiene amplios poderes dentro de ese contexto de contenedor, y ocurren malas configuraciones.
Ejecutar como no root reduce el radio de daño y te obliga a resolver correctamente los permisos de volúmenes en lugar de parchearlos.

7) ¿Por qué funciona en macOS/Windows Docker Desktop pero falla en Linux?

Docker Desktop usa una VM y un mecanismo diferente de compartir sistema de archivos. La propiedad y semánticas de permisos pueden ser traducidas o simplificadas.
Los hosts Linux son “reales” en el sentido de que el bind mount es el sistema de archivos real con UIDs reales, ACLs y módulos de seguridad.

8) ¿Cómo manejo múltiples contenedores escribiendo en el mismo volumen?

Prefiere una estrategia de GID compartido (escritura por grupo + setgid) o usa ACLs para otorgar escritura a múltiples UIDs.
Evita ejecutar todo como el mismo UID a menos que estés preparado para las consecuencias de auditoría y depuración.

9) ¿Y si uso Kubernetes en lugar de Docker Compose?

Los principios son los mismos: alinea UID/GID de ejecución con los permisos del almacenamiento. Kubernetes añade opciones en securityContext como
runAsUser y fsGroup. Cuidado: fsGroup puede disparar cambios recursivos de permisos en algunos tipos de volúmenes,
lo cual es excelente para volúmenes pequeños y doloroso para volúmenes enormes.

10) ¿Umask forma parte de esta historia?

Sí. Incluso si la propiedad del directorio es correcta, una umask restrictiva puede crear archivos que otros procesos no pueden leer.
Cuando tienes sidecars o lectores compartidos, decide intencionalmente si necesitas lectura/escritura por grupo y ajusta la umask acorde.

Conclusión: próximos pasos que perduran

“Permission denied” en volúmenes Docker rara vez es misterioso. Casi siempre es UID/GID numérico desajustado, falta de permiso de grupo/ACL,
o una capa de seguridad (SELinux, NFS root-squash) haciendo su trabajo.
La solución que perdura es dejar de improvisar y empezar a declarar identidad y propiedad como parte de tu contrato de despliegue.

Haz esto a continuación (en orden)

  1. Elige un UID/GID numérico estable para cada servicio con estado y anótalo en el repositorio.
  2. Provisiona directorios escribibles intencionalmente (propietario/modo, o ACLs, o grupo+setgid), no por accidente.
  3. Ejecuta contenedores como ese UID/GID y verifica con una prueba de escritura simple durante el arranque o en CI.
  4. Elimina chown recursivo de entrypoints a menos que tu ruta de datos sea pequeña y disfrutes despliegues lentos.
  5. Considera SELinux y almacenamiento en red desde temprano; no son más fáciles a las 2 a.m.

Si tratas los volúmenes como parte de la aplicación—no como un pensamiento posterior—dejarás de jugar al whack-a-mole de permisos.
Tu yo del futuro seguirá recibiendo paginaciones, por supuesto. Solo por algo más interesante.

← Anterior
¿Realtek Audio no funciona? La solución que supera reinstalar controladores
Siguiente →
Windows no arranca después de una actualización: recupere en 15 minutos sin reinstalar

Deja un comentario