Cambiar tema

Guía completa de permisos en directorios montados de Docker: 5 soluciones del diagnóstico a la práctica

Easton editorial illustration: one mounted folder passing through an ownership lock into a container

Ese mensaje rojo en la terminal: «Permission denied». Por quinta vez esta noche. El contenedor de desarrollo funcionaba perfecto en Mac; al desplegar en el servidor Linux de producción, todo explota. Intentas borrar los archivos de log generados por el contenedor y el sistema te dice «sin permiso». Pero si eres el administrador del servidor.

Ayer un compañero comentó al pasar: «¿No basta con chmod 777?» Lo probaste y, efectivamente, funcionó. Pero una voz te recuerda: ¿de verdad no hay problema con esto?

Según estadísticas del foro de la comunidad Docker, el 40 % de usuarios principiantes encuentra problemas de permisos en directorios montados; el 60 % elige chmod 777. Resultado: riesgo de escape de contenedores y filtración de datos.

40%
Usuarios con problemas de permisos
40% principiantes los encuentran, 60% elige chmod 777

Este artículo te ayuda a entender la esencia de los permisos en Docker: qué pasa con UID y GID. Luego te da 5 soluciones serias, desde el parche temporal más simple hasta la configuración de seguridad de nivel empresarial. Aprenderás a diagnosticar el problema con 3 comandos.

Causa raíz: por qué hay problemas de permisos

UID/GID son la verdadera identidad

¿Crees que Linux identifica a las personas por nombre de usuario? Error. El kernel solo reconoce números: UID (ID de usuario) y GID (ID de grupo). El nombre de usuario es solo un apodo para humanos.

Por ejemplo, en tu máquina ejecuta id:

uid=1000(oden) gid=1000(oden) groups=1000(oden)

¿Lo ves? 1000 es tu identificador real. El nombre oden no le importa al kernel.

Mira root:

uid=0(root) gid=0(root) groups=0(root)

El usuario 0 es el superusuario. Da igual cómo se llame: si el UID es 0, tiene los permisos máximos del sistema.

Cómo surge el conflicto de permisos

Aquí está el núcleo del problema. Cuando ejecutas Docker en el host con un usuario normal (por ejemplo UID=1000) pero el contenedor corre por defecto como root (UID=0), surge la contradicción.

La cadena completa del conflicto:

  1. En el host Linux, arrancas el contenedor como usuario normal (UID=1000)
  2. Los procesos dentro del contenedor corren por defecto como root (UID=0)
  3. root dentro del contenedor crea un archivo, por ejemplo /app/logs/output.log
  4. Ese archivo se mapea al host mediante bind mount: ./logs/output.log
  5. En el host, el owner del archivo aparece como root (UID=0)
  6. Como usuario normal (UID=1000), ¿quieres borrarlo? Imposible, no tienes permisos

Así de directo. El contenedor no sabe quién eres en el host; solo reconoce UID. Un archivo creado por el 0, un usuario distinto de 0 no puede tocarlo.

¿Por qué en Mac y Windows no pasa?

Puede que pienses: «Qué raro, en Mac con Docker nunca me ha pasado».

Correcto, porque Docker Desktop en Mac y Windows corre dentro de una máquina virtual. Mac usa Apple Virtualization Framework (antes hyperkit); Windows usa WSL2 o Hyper-V. Hay una capa extra de «conversión de permisos».

El sistema de archivos VirtioFS de Mac convierte automáticamente el owner de los archivos generados por el contenedor al usuario actual del host. Suena cómodo, ¿verdad? Sí, pero también explica por qué tu código va bien en Mac y revienta en el servidor Linux: en Linux, Docker llama directamente al kernel, sin esa capa intermedia.

En resumen, Docker Desktop sacrifica algo de «realismo» por la experiencia de usuario. En desarrollo no lo notas; al desplegar, te llevas la sorpresa.

Otros detalles a tener en cuenta

Bind mount vs Named Volume:

  • Bind mount (-v /host/path:/container/path) mapea directamente un directorio del host; los problemas de permisos son más evidentes
  • Named Volume (-v mydata:/container/path) lo gestiona Docker; los permisos suelen ser más flexibles, pero tampoco están libres de problemas

SELinux y AppArmor:
Si tu Linux tiene SELinux (CentOS/RHEL) o AppArmor (Ubuntu), los permisos se complican. Además del emparejamiento UID/GID, hay que considerar las etiquetas de contexto de seguridad. ¿Errores de permisos inexplicables? Revisa los logs de SELinux:

sudo ausearch -m avc -ts recent

Tu usuario no existe dentro del contenedor:
Las imágenes suelen traer solo root y unos pocos usuarios del sistema. El usuario con UID=1000 de tu host, el contenedor no lo conoce. Por eso el owner del archivo aparece como una cadena de números.

Diagnóstico rápido: 3 comandos para localizar el problema

Ante Permission denied, no entres en pánico. ¿Cómo lo hacen los profesionales? Tres comandos, un minuto.

Comando 1: ver el owner real del archivo

ls -ln /your/mount/path

Ojo: es -ln, no -l. ¿La diferencia? -l muestra nombres de usuario; -ln muestra UID/GID numéricos.

Ejemplo de salida:

-rw-r--r-- 1 0 0 1024 Dec 17 10:00 output.log

¿Cómo leerlo?

  • Primera columna -rw-r--r--: bits de permiso (no es lo central)
  • Segunda columna 1: número de enlaces duros (poco relevante)
  • Tercera columna 0: UID del owner ← aquí está la clave
  • Cuarta columna 0: GID del owner ← y aquí
  • Después: tamaño, fecha, nombre del archivo

¿Ves 0 0? Eso es root. Si tu UID en el host es 1000, claro que no puedes modificar ese archivo.

Compara con lo normal:

ls -ln ~/my-project

Salida:

-rw-r--r-- 1 1000 1000 2048 Dec 17 11:30 README.md

Ahí está: 1000 1000. Esos son tus archivos.

Comando 2: ver la identidad real del proceso del contenedor

docker exec <container_name> id

Ejemplo de salida:

uid=0(root) gid=0(root) groups=0(root)

Te dice con qué identidad corren los procesos del contenedor. Normalmente root (UID=0).

Compara con el host:

id

Salida:

uid=1000(oden) gid=1000(oden) groups=1000(oden),4(adm),27(sudo)

¿Ves la diferencia? Contenedor 0, host 1000. No coinciden. Ahí nace el conflicto.

Comando 3: revisar la configuración de montaje de Docker

docker inspect <container_name> | grep -A 10 "Mounts"

Salida similar a:

"Mounts": [
    {
        "Type": "bind",
        "Source": "/home/oden/project/logs",
        "Destination": "/app/logs",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
]

¿Qué mirar?

  • Type: ¿bind o volume? Los bind mount suelen dar más problemas de permisos
  • Source: ruta en el host; haz ls -ln en esa ruta para ver el owner
  • RW: true = lectura/escritura; false = solo lectura
  • Mode: opciones especiales de montaje (por ejemplo :z o :Z para SELinux)

Flujo de diagnóstico en un minuto

Ante un problema de permisos, sigue este orden:

  1. Primero el archivo: ls -ln para ver UID/GID del archivo problemático
  2. Luego el contenedor: docker exec <container> id para ver la identidad del proceso
  3. Compara: si el UID del contenedor y el owner del archivo difieren de tu UID en el host, hay conflicto
  4. Confirma la config: docker inspect para ver tipo de montaje y rutas

Ejemplo real. No puedes borrar logs del contenedor:

# Paso 1: ver owner del archivo
$ ls -ln ./logs/
-rw-r--r-- 1 0 0 5120 Dec 17 12:00 app.log

# UID=0, creado por root

# Paso 2: ver identidad del contenedor
$ docker exec myapp id
uid=0(root) gid=0(root) groups=0(root)

# El contenedor corre como root

# Paso 3: ver tu identidad
$ id
uid=1000(oden) gid=1000(oden) ...

# Yo soy 1000, el contenedor 0, ¡no coinciden!

# Diagnóstico: el contenedor corre como root, genera archivos de root, yo no puedo borrarlos

Con este diagnóstico ya sabes qué solución aplicar. Sigue leyendo.

5 soluciones: elige la que mejor encaje

Ya conoces la causa y el diagnóstico. Ahora a resolverlo. Te presento 5 enfoques, de simple a complejo, de parche temporal a configuración empresarial. Lo importante es saber qué escenario requiere qué solución.

Solución 1: especificar UID/GID en tiempo de ejecución con —user

Para quién: pruebas rápidas o entorno de desarrollo local

Principio: le dices a Docker «ejecuta con mi UID»; los archivos que genere el contenedor serán tuyos.

Cómo usarlo:

# Línea de comandos
docker run --user $(id -u):$(id -g) -v /host/data:/app/data myimage

# docker-compose.yml
services:
  myapp:
    image: myimage
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./data:/app/data

Al ejecutar:

export UID=$(id -u)
export GID=$(id -g)
docker-compose up

Ventajas:

  • Lo más simple, efecto inmediato
  • No hace falta tocar Dockerfile ni reconstruir la imagen
  • Ideal para iterar rápido en local

Desventajas:

  • Hay que especificarlo en cada arranque
  • Si la app depende de un UID concreto (nginx y el puerto 80 requieren root), fallará
  • El UID de cada miembro del equipo puede variar; no se puede fijar en duro

Índice de riesgo: bajo

Sistemas: Linux perfecto; Mac/Windows funcionan pero la experiencia es peor (capa de VM)

Cuándo usarlo: desarrollo local, pruebas temporales, validar ideas rápido. Por ejemplo, desarrollas en Mac, en CI Linux aparece el problema de permisos: esta solución te saca del apuro.


Solución 2: crear un usuario coincidente en el Dockerfile

Para quién: imágenes compartidas en equipo, escenarios de uso repetido

Principio: en el build pasas el UID del host como build arg y creas el usuario correspondiente en la imagen. Al arrancar, el contenedor corre con esa identidad.

Cómo usarlo:

Dockerfile:

FROM python:3.11

# Recibir argumentos de build
ARG UID=1000
ARG GID=1000

# Crear grupo y usuario
RUN groupadd -g $GID appuser && \
    useradd -m -u $UID -g $GID appuser

# Directorio de trabajo y permisos
WORKDIR /app
RUN chown -R appuser:appuser /app

# Cambiar a usuario no root
USER appuser

# Comandos posteriores como appuser
COPY --chown=appuser:appuser . /app
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

Build:

docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t myapp:latest .

docker-compose.yml:

services:
  myapp:
    build:
      context: .
      args:
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    volumes:
      - ./data:/app/data

Ventajas:

  • Un build, correcto en cada ejecución
  • Entorno de usuario completo en el contenedor (home, shell, etc.)
  • Enfoque profesional, nivel producción

Desventajas:

  • Requiere modificar el Dockerfile
  • Si el UID del equipo varía, cada uno debe construir su imagen (no compartir la misma)
  • Si la app necesita root al arrancar (configuración del sistema), no sirve

Índice de riesgo: bajo

Sistemas: Linux perfecto; Mac/Windows tienen matices por la VM, pero funciona

Cuándo usarlo: imagen base estándar del equipo; o imagen para distribuir (proyecto open source) donde cada usuario construye con su UID.


Solución 3: ajuste dinámico con script entrypoint (enfoque gosu)

Para quién: la app necesita arrancar como root e inicializar, luego ejecutar con menos privilegios

Principio: el contenedor arranca como root con un script entrypoint que crea el usuario dinámicamente y usa gosu (similar a sudo pero más seguro) para pasar al usuario objetivo y ejecutar el programa principal.

Cómo usarlo:

Dockerfile:

FROM node:18

# Instalar gosu
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*

# Copiar script entrypoint
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /app
COPY . /app

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["node", "server.js"]

entrypoint.sh:

#!/bin/bash
set -e

# Si se especifica LOCAL_USER_ID
if [ -n "$LOCAL_USER_ID" ]; then
    # Crear usuario (si no existe)
    useradd -u $LOCAL_USER_ID -o -m appuser 2>/dev/null || true

    # Cambiar owner de /app
    chown -R appuser:appuser /app

    # gosu cambia a appuser y ejecuta el resto
    exec gosu appuser "$@"
else
    # Sin variable, ejecutar como root
    exec "$@"
fi

Ejecución:

docker run -e LOCAL_USER_ID=$(id -u) -v ./data:/app/data myapp

Ventajas:

  • Máxima flexibilidad: root para init, usuario normal para el runtime
  • La misma imagen sirve para distintos UID
  • Buena seguridad (gosu es más seguro que su/sudo)

Desventajas:

  • Hay que modificar Dockerfile y entrypoint
  • Más complejidad y mantenimiento
  • gosu requiere instalación extra (aunque es ligero)

Índice de riesgo: medio (gosu es herramienta recomendada por Docker)

Sistemas: todos

Cuándo usarlo: la app necesita configurar el sistema al arrancar (root) pero debe correr como usuario normal; nginx y el puerto 80 (root) con workers en baja privilegio; o init de esquema de BD (root) y luego servicio con menos permisos.


Solución 4: User Namespace Remapping (userns-remap)

Para quién: normativa de seguridad corporativa que exige aislamiento y prohíbe root real en contenedores

Principio: configuración a nivel del daemon de Docker que remapea automáticamente todos los UID de contenedores a un rango de «subusuarios». Dentro del contenedor creen ser root (UID=0); en el host son un usuario normal (por ejemplo UID=100000).

Cómo usarlo:

Edita /etc/docker/daemon.json:

{
  "userns-remap": "default"
}

Reinicia Docker:

sudo systemctl restart docker

Docker crea el usuario dockremap y asigna rangos en /etc/subuid y /etc/subgid.

Verificación:

# Arrancar contenedor
docker run -d --name test -v /tmp/test:/data busybox sleep 3600

# Dentro parece root
docker exec test id
# uid=0(root) gid=0(root)

# En el host
ls -ln /tmp/test
# owner es un número grande, por ejemplo 100000

Ventajas:

  • Una sola configuración, efecto global
  • Todos los contenedores aislados sin tocar imágenes ni comandos
  • Máxima seguridad: escape de contenedor = shell de subusuario, no root real
  • Solución empresarial recomendada por Docker

Desventajas:

  • Configuración a nivel de sistema, afecta a todos los contenedores
  • Contenedores y volúmenes existentes pueden ser incompatibles; hay que reconstruir
  • No compatible con modo rootless a la vez
  • Algunas operaciones privilegiadas (mount) siguen sin funcionar

Índice de riesgo: bajo (recomendación oficial)

Sistemas: solo Linux (requiere soporte de user namespace en el kernel)

Cuándo usarlo: normativa que exige aislamiento; entorno multiinquilino con imágenes no confiables; quieres una solución definitiva sin configurar cada proyecto.


Solución 5: Rootless Docker

Para quién: máximos requisitos de seguridad, aceptando algunas limitaciones funcionales

Principio: el daemon de Docker corre como usuario no root. Todos los contenedores viven en el namespace de ese usuario, totalmente aislados del root del sistema.

Cómo usarlo:

Instalar Rootless Docker:

# Desinstalar Docker root (si existe)
sudo apt-get remove docker docker-engine docker.io

# Instalar Rootless Docker
curl -fsSL https://get.docker.com/rootless | sh

# Configurar variables de entorno según las instrucciones
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

# Arrancar
systemctl --user start docker
systemctl --user enable docker

Verificación:

docker run hello-world
# Todo corre sin privilegios root

Ventajas:

  • Seguridad máxima: ni el daemon ni los contenedores son root
  • Escape de contenedor limitado a los permisos de tu usuario
  • Ideal para imágenes no confiables, multiinquilino, entornos sensibles

Desventajas:

  • No puedes usar puertos privilegiados (<1024, incluidos 80/443)
  • Algunos modos de red no disponibles (por ejemplo host)
  • Rendimiento ligeramente inferior (overhead de namespaces)
  • Configuración más compleja, documentación más escasa

Índice de riesgo: bajo (diseño sólido, soporte oficial de Docker)

Sistemas: Linux moderno (newuidmap/newgidmap; Ubuntu 20.04+, CentOS 8+)

Cuándo usarlo: normativa muy estricta (finanzas, sanidad); imágenes de terceros no confiables; clúster K8s con pods no root y Docker rootless en máquinas de producción.


Decisión rápida: ¿cuál elijo?

Tras las 5 soluciones, ¿sigues sin decidir? Usa este árbol:

¿Problema de permisos?
├─ ¿Solo una prueba temporal?
│  └─ Sí → Solución 1 (--user)

├─ ¿Proyecto de equipo a largo plazo?
│  ├─ ¿La app necesita root al arrancar?
│  │  └─ Sí → Solución 3 (entrypoint+gosu)
│  └─ ¿No necesita root?
│     └─ Solución 2 (usuario en Dockerfile)

├─ ¿Normativa exige aislamiento forzado?
│  ├─ ¿Necesitas puertos privilegiados u otras funciones?
│  │  └─ Sí → Solución 4 (userns-remap)
│  └─ ¿No necesitas privilegios?
│     └─ Solución 5 (Rootless Docker)

└─ ¿Solo quieres arreglar desarrollo local?
   └─ Solución 1 (--user)

Mi recomendación:

  • Desarrollo: solución 1 (rápida y efectiva)
  • Proyecto de equipo: solución 2 o 3 (profesional y ordenada)
  • Producción: solución 4 o 5 (seguridad primero)

No uses la solución más compleja por defecto. Elige según tu necesidad real. Basta con lo suficiente.

Casos especiales multiplataforma: Mac, Windows y Linux

«En mi máquina funciona»

¿Te suena? En Mac todo va bien; en el servidor Linux explota. O al revés: en Linux bien, en Windows fenómenos raros.

La razón: las implementaciones de Docker difieren mucho entre las tres plataformas.

Linux: lo más «real» y donde más problemas hay

En Linux, Docker llama directamente al kernel, sin VM intermedia. Es lo más parecido a producción, pero también donde los permisos se ven con más claridad.

Características:

  • Contenedor y host comparten el mismo kernel
  • UID/GID se mapean directamente, sin conversión
  • Por defecto el contenedor corre como root (UID=0)
  • Los conflictos de bind mount salen a la luz de inmediato

Buenas prácticas:

  • En desarrollo: solución 1 (—user) para salir del paso
  • Proyecto a largo plazo: solución 2 (usuario en Dockerfile)
  • Producción: solución 4 o 5 (userns-remap o rootless)

Trampa habitual:

# No puedes borrar archivos del contenedor
rm: cannot remove 'logs/app.log': Permission denied

# Ver owner
ls -ln logs/
# -rw-r--r-- 1 0 0 ...

# Causa: contenedor como root, archivos de root

Solución: añade user: "${UID}:${GID}" en docker-compose.yml.

Mac: permisos «relajados» con trampas

Docker Desktop en Mac corre en una VM ligera (Apple Virtualization Framework). El sistema de archivos VirtioFS convierte permisos automáticamente.

Características:

  • Los archivos del contenedor suelen convertirse al usuario actual del host
  • En la mayoría de casos no notas problemas de permisos
  • Esa «comodidad» te engaña al desplegar

Problemas conocidos:

  • VirtioFS tuvo varios bugs de permisos en 2023-2024 (directorios anidados mal configurados)
  • Docker Desktop 4.13+ corrigió la mayoría, pero quedan casos límite
  • Enlaces simbólicos en varias capas pueden perder permisos

Buenas prácticas:

  • Desarrollo local: disfruta la comodidad, sin configuración extra
  • No confíes solo en eso: en el Dockerfile usa solución 2
  • Antes de desplegar, prueba en Linux (o VM)

Trampa habitual:

# En Mac esto funciona
services:
  app:
    image: myapp
    volumes:
      - ./data:/app/data
# Contenedor como root, pero el owner del archivo eres tú

# En Linux al desplegar, revienta
# Archivos de root, CI sin acceso

Solución: aunque en Mac no falle, añade user:

services:
  app:
    user: "${UID:-1000}:${GID:-1000}"

Windows: el escenario más complejo

Docker Desktop en Windows corre en WSL2 o Hyper-V. El modelo de permisos NTFS y las ACL de Linux no son lo mismo.

Características:

  • Modo WSL2: relativamente parecido a Linux, pero conversión al cruzar NTFS y ext4
  • Modo Hyper-V: otra capa de virtualización, conversión más compleja
  • BitLocker en algunas unidades complica aún más los permisos

Problemas frecuentes:

# bind mount a unidad C
docker run -v C:\Users\oden\project:/app myimage
# Permisos caóticos, a veces lectura sin escritura

# bind mount a ruta WSL
docker run -v /mnt/c/Users/oden/project:/app myimage
# Mejor, pero sigue habiendo problemas

Buenas prácticas:

  • Prioriza Named Volume en lugar de bind mount:
  services:
    db:
      image: postgres
      volumes:
        - pgdata:/var/lib/postgresql/data  # volume
  volumes:
    pgdata:  # Docker gestiona, evita NTFS
  
  • Si necesitas bind mount, proyecto dentro del filesystem WSL2 (\\wsl$\Ubuntu\home\...)
  • Evita montajes entre unidades

Problemas conocidos:

  • Montaje en C: u otras particiones NTFS puede mostrar 777 en bits (asusta pero NTFS manda)
  • Enlaces simbólicos con soporte limitado en Windows
  • LF vs CRLF mezclados con Git y Docker

Equipo multiplataforma: estrategia unificada

¿Mac, Linux y Windows en el mismo equipo?

Configuración recomendada:

docker-compose.yml:

services:
  app:
    build:
      context: .
      args:
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./src:/app/src

Dockerfile:

FROM node:18

ARG UID=1000
ARG GID=1000

RUN groupadd -g $GID appuser && \
    useradd -m -u $UID -g $GID appuser

WORKDIR /app
RUN chown appuser:appuser /app

USER appuser

.env.example (compartido en equipo):

# Linux/Mac
# export UID=$(id -u)
# export GID=$(id -g)

# Windows puede fijar valores
UID=1000
GID=1000

README.md:

## Arrancar el proyecto

**Linux/Mac**:
```bash
export UID=$(id -u) GID=$(id -g)
docker-compose up

Windows:

# En WSL2, o docker-compose up directo (default 1000)
docker-compose up

**Puntos clave**:
- build args y variables de entorno para flexibilidad
- Linux pasa UID real; Mac/Windows usan default
- Usuario en Dockerfile para consistencia multiplataforma
- Documentar diferencias por plataforma

### Resumen en una frase

- **Linux**: problemas más visibles, más soluciones, más parecido a producción
- **Mac**: suele ir bien, pero no te confíes; configura bien igualmente
- **Windows**: prioriza volume sobre bind mount; proyecto en filesystem WSL2

¿Equipo multiplataforma? build args y user para que todos puedan trabajar.

## Casos prácticos: escenarios habituales resueltos

Tras teoría, diagnóstico, soluciones y multiplataforma, cinco escenarios reales paso a paso.

### Caso 1: desarrollo local, no puedes borrar logs del contenedor

**Síntoma**:
Corres un contenedor de app que genera logs. Tras un tiempo quieres limpiar:
```bash
rm -rf logs/
# rm: cannot remove 'logs/app.log': Permission denied

Diagnóstico:

# Paso 1: owner del archivo
$ ls -ln logs/
total 1024
-rw-r--r-- 1 0 0 524288 Dec 17 14:30 app.log
-rw-r--r-- 1 0 0 524288 Dec 17 14:31 error.log

# UID=0, creado por root

# Paso 2: identidad del contenedor
$ docker exec myapp id
uid=0(root) gid=0(root) groups=0(root)

# Contenedor como root

# Paso 3: tu identidad
$ id
uid=1000(oden) gid=1000(oden) groups=1000(oden)

# Yo 1000, contenedor 0, ¡no coinciden!

Solución:
Modifica docker-compose.yml con user:

services:
  myapp:
    image: myapp:latest
    user: "${UID:-1000}:${GID:-1000}"  # línea clave
    volumes:
      - ./logs:/app/logs

Ejecución:

export UID=$(id -u)
export GID=$(id -g)
docker-compose down
docker-compose up

El contenedor corre con tu UID; los logs serán tuyos.

En resumen: una línea user y listo.


Caso 2: Django/Flask, permisos en archivos estáticos

Síntoma:
Tu app web Python recoge estáticos. Tras collectstatic:

docker exec webapp python manage.py collectstatic
# Se crea la carpeta static/

ls -ln static/
# drwxr-xr-x 1 0 0 ...
# owner root; CI o contenedor nginx sin acceso

Causa:
Contenedor como root; archivos de root. Si nginx sirve esos archivos, su usuario puede no tener lectura.

Solución:
Crea usuario de aplicación en el Dockerfile:

FROM python:3.11

# Usuario de aplicación
RUN groupadd -g 1000 appuser && \
    useradd -m -u 1000 -g 1000 appuser

WORKDIR /app

# Dependencias (aún root, apt-get, etc.)
COPY requirements.txt .
RUN pip install -r requirements.txt

# Código con owner correcto
COPY --chown=appuser:appuser . /app

USER appuser

CMD ["gunicorn", "myapp.wsgi:application"]

docker-compose.yml:

services:
  webapp:
    build: .
    volumes:
      - static_volume:/app/static

  nginx:
    image: nginx:alpine
    volumes:
      - static_volume:/usr/share/nginx/html/static:ro  # solo lectura
    ports:
      - "80:80"

volumes:
  static_volume:

Puntos clave:

  • Usuario con UID=1000 en Dockerfile
  • Named Volume para estáticos, no bind mount
  • nginx lee el volume con su usuario; Docker gestiona permisos

En resumen: usuario en Dockerfile + volume compartido.


Caso 3: permisos en volúmenes de base de datos

Síntoma:
Al arrancar PostgreSQL o MySQL:

docker-compose up postgres
# postgres: could not open file "/var/lib/postgresql/data/...": Permission denied

Causa:
Las imágenes de BD suelen cambiar a un UID concreto (postgres usa UID=999). Con bind mount, el owner del directorio en el host puede no coincidir.

Diagnóstico:

# Directorio montado
ls -ln ./pgdata
# drwxr-xr-x 1 1000 1000 ...
# owner 1000, pero postgres necesita 999

# Usuario de la imagen postgres
docker run --rm postgres:15 id
# uid=999(postgres) gid=999(postgres) groups=999(postgres)

Solución:

Método A: Named Volume (recomendado)

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data  # volume, no bind mount

volumes:
  pgdata:  # Docker gestiona permisos

Método B: si necesitas bind mount, prepara permisos antes

mkdir -p ./pgdata
sudo chown -R 999:999 ./pgdata  # coincide con UID/GID de postgres

docker-compose.yml:

services:
  postgres:
    image: postgres:15
    volumes:
      - ./pgdata:/var/lib/postgresql/data

Nota: el UID varía por imagen y versión:

  • PostgreSQL: 999
  • MySQL: 999
  • MongoDB: 999
  • Redis: 999 (curiosamente, muchos usan 999)

No lo des por hecho; confirma con docker run --rm <image> id.

En resumen: BD con Named Volume; bind mount solo con chown previo.


Caso 4: permisos incorrectos en artefactos de CI

Síntoma:
En CI:

# .gitlab-ci.yml
build:
  script:
    - docker run --rm -v $CI_PROJECT_DIR:/app builder npm run build
    - ls -l dist/  # ver artefactos
    # -rw-r--r-- 1 root root ... (owner root)
    - cp dist/* /deploy/  # Permission denied!

El runner corre como usuario normal; el contenedor construye como root; los pasos siguientes no pueden acceder.

Solución:

Método A: cambiar owner explícitamente en el contenedor de build

# Dockerfile.builder
FROM node:18

WORKDIR /app
COPY package*.json ./
RUN npm install

COPY . .

# Build y cambio de owner
RUN npm run build && \
    chown -R 1000:1000 /app/dist

CMD ["npm", "run", "build"]

Método B: —user en el contenedor de build

# .gitlab-ci.yml
build:
  script:
    - docker run --rm --user $(id -u):$(id -g) -v $CI_PROJECT_DIR:/app builder npm run build
    - ls -l dist/  # owner correcto
    - cp dist/* /deploy/  # sin problema

Método C: entrypoint (más flexible)

FROM node:18

RUN apt-get update && apt-get install -y gosu

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh

WORKDIR /app
ENTRYPOINT ["/entrypoint.sh"]
CMD ["npm", "run", "build"]

entrypoint.sh:

#!/bin/bash
set -e

npm run build

# Si OUTPUT_UID está definido, cambiar owner de artefactos
if [ -n "$OUTPUT_UID" ]; then
    chown -R $OUTPUT_UID:${OUTPUT_GID:-$OUTPUT_UID} /app/dist
fi

CI:

build:
  script:
    - docker run --rm -e OUTPUT_UID=$(id -u) -v $CI_PROJECT_DIR:/app builder

En resumen: fija owner de artefactos en build o usa —user.


Caso 5: permisos en Pod de Kubernetes

Síntoma:
Despliegas en K8s y el Pod falla:

kubectl logs mypod
# Error: EACCES: permission denied, open '/app/data/config.json'

Causa:
securityContext del Pod limita el usuario de ejecución, o fsGroup del volume está mal.

Diagnóstico:

# Entrar al Pod
kubectl exec -it mypod -- id
# uid=1000 gid=1000 groups=1000

# Archivos en el volume
kubectl exec -it mypod -- ls -ln /app/data
# drwxr-xr-x 2 0 0 ...
# owner root, Pod corre como 1000, no puede leer

Solución:

securityContext en el Pod:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  securityContext:
    runAsUser: 1000      # Pod con UID=1000
    runAsGroup: 1000     # GID=1000
    fsGroup: 1000        # group del volume, lectura/escritura

  containers:
  - name: app
    image: myapp:latest
    volumeMounts:
    - name: data
      mountPath: /app/data

  volumes:
  - name: data
    emptyDir: {}

Puntos clave:

  • runAsUser: UID del proceso del contenedor
  • runAsGroup: GID del proceso
  • fsGroup: group owner de archivos en el volume, con acceso R/W

Con PersistentVolumeClaim:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mypvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

---
apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  securityContext:
    fsGroup: 1000  # group 1000 en el PVC

  containers:
  - name: app
    image: myapp:latest
    securityContext:
      runAsUser: 1000  # proceso como 1000
    volumeMounts:
    - name: storage
      mountPath: /app/data

  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: mypvc

En resumen: runAsUser y fsGroup claros en securityContext.


Resumen de los 5 casos

EscenarioSíntomaSoluciónMétodo recomendado
Logs locales no se borranPermission deniedconfig useruser en docker-compose.yml
Recogida de estáticosnginx sin lecturausuario en DockerfileUSER appuser + volume
BD no arrancadirectorio de datos sin escrituraNamed VolumeDocker gestiona permisos
Artefactos CIpasos siguientes sin acceso—user o entrypointchown en build
Pod K8serror EACCESsecurityContextrunAsUser + fsGroup

Cada escenario pide un enfoque distinto. Diagnostica primero, luego aplica la solución correcta.

Conclusión

¿Recuerdas la escena de las tres de la madrugada? «Permission denied» en pantalla y tú, administrador, sin poder borrar un archivo.

Ahora lo entiendes:

Causa raíz: Linux solo reconoce UID/GID, no nombres. Archivos creados por root (UID=0) en el contenedor, un usuario normal (UID=1000) en el host no puede tocarlos.

Diagnóstico: tres comandos — ls -ln para owner, docker exec <container> id para identidad del contenedor, docker inspect para montajes. Un minuto para localizar el problema.

Soluciones: cinco opciones:

  1. —user: parche rápido, pruebas locales
  2. Usuario en Dockerfile: profesional, proyectos de equipo
  3. entrypoint+gosu: init como root, runtime con menos privilegios
  4. userns-remap: aislamiento empresarial
  5. Rootless Docker: seguridad máxima, con limitaciones

Multiplataforma: Mac y Windows tienen capa de conversión; en Linux el conflicto es directo. No te confíes por la comodidad en Mac; configura bien en el Dockerfile.

Práctica: cinco casos — desarrollo local, estáticos, BD, CI, K8s — cada uno con su mejor enfoque.

Empieza hoy

Hoy (5 minutos):

  • Añade user: "${UID:-1000}:${GID:-1000}" a tu docker-compose.yml
  • Con docker exec <container> id mira el UID real de tus contenedores
  • Prueba ls -ln en un problema de permisos

Esta semana (1-2 horas):

  • Mejora el Dockerfile con ARG UID/GID y creación de usuario
  • Documenta los comandos de diagnóstico en wiki o README
  • Comparte este artículo con el equipo si te resultó útil

A largo plazo:

  • Si la normativa lo exige, evalúa userns-remap o rootless
  • Revisa configuración Docker en producción
  • Añade comprobaciones de permisos en CI/CD

Para cerrar

Los permisos parecen técnicos y aburridos, pero en el fondo es «autenticación de identidad». El contenedor no sabe quién eres en el host; solo ve números.

Cuando entiendes el mapeo UID/GID, todo se simplifica. Sin chmod 777 a ciegas, sin madrugadas peleando con permisos.

Elige la solución correcta, usa los comandos adecuados, entiende el porqué.

Resuelto.

FAQ

¿Por qué un fichier créé par Docker appartient-il parfois à root sur l'hôte ?
Le noyau compare los UID y GID, pas los noms d'utilisateur. Si el processus del conteneur s'exécute con l'UID 0, los fichiers d'un bind mount peuvent donc apparaître comme appartenant à root en l'hôte.
¿Por qué éviter chmod 777 pour corriger un problème de volume Docker ?
Cette commande donne de droits d'écriture à tout el monde y masque la cause réelle. Il vaut mieux aligner los UID/GID con --user, créer un utilisateur dédié en l'image o choisir un volume nommé selon el scénario.
¿Qué solution privilégier en production ?
Exécutez el conteneur con un utilisateur non root défini en el Dockerfile y donnez-lui uniquement los droits nécessaires. Pour un bind mount, alignez son UID/GID con l'hôte y validez los permissions avant el démarrage.

21 min de lectura · Publicado el: 17 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog