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

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.
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:
- En el host Linux, arrancas el contenedor como usuario normal (UID=1000)
- Los procesos dentro del contenedor corren por defecto como root (UID=0)
- root dentro del contenedor crea un archivo, por ejemplo
/app/logs/output.log - Ese archivo se mapea al host mediante bind mount:
./logs/output.log - En el host, el owner del archivo aparece como root (UID=0)
- 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 permisosSource: ruta en el host; hazls -lnen esa ruta para ver el ownerRW: true = lectura/escritura; false = solo lecturaMode: opciones especiales de montaje (por ejemplo:zo:Zpara SELinux)
Flujo de diagnóstico en un minuto
Ante un problema de permisos, sigue este orden:
- Primero el archivo:
ls -lnpara ver UID/GID del archivo problemático - Luego el contenedor:
docker exec <container> idpara ver la identidad del proceso - Compara: si el UID del contenedor y el owner del archivo difieren de tu UID en el host, hay conflicto
- Confirma la config:
docker inspectpara 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 contenedorrunAsGroup: GID del procesofsGroup: 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
| Escenario | Síntoma | Solución | Método recomendado |
|---|---|---|---|
| Logs locales no se borran | Permission denied | config user | user en docker-compose.yml |
| Recogida de estáticos | nginx sin lectura | usuario en Dockerfile | USER appuser + volume |
| BD no arranca | directorio de datos sin escritura | Named Volume | Docker gestiona permisos |
| Artefactos CI | pasos siguientes sin acceso | —user o entrypoint | chown en build |
| Pod K8s | error EACCES | securityContext | runAsUser + 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:
- —user: parche rápido, pruebas locales
- Usuario en Dockerfile: profesional, proyectos de equipo
- entrypoint+gosu: init como root, runtime con menos privilegios
- userns-remap: aislamiento empresarial
- 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> idmira el UID real de tus contenedores - Prueba
ls -lnen 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 ?
¿Por qué éviter chmod 777 pour corriger un problème de volume Docker ?
¿Qué solution privilégier en production ?
21 min de lectura · Publicado el: 17 dic 2025 · Actualizado el: 21 ago 2026
Guía práctica de Docker
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Docker: comparación de montajes — Volume vs Bind Mount con pruebas de rendimiento
Comparación profunda de los tres modos de montaje en Docker: soluciona npm install 3 veces más lento en Mac, con árbol de decisión y escenarios reales para elegir Volume, Bind Mount o tmpfs
Parte 16 de 38
Siguiente
Modos de red Docker: bridge, host, none y container — rendimiento y cuándo usar cada uno
Análisis profundo de los cuatro modos de red Docker (bridge, host, none y container): principios, comparación de rendimiento y escenarios de uso. Te ayuda a elegir la configuración correcta, con casos prácticos y guía de decisión.
Parte 18 de 38



Comentarios
Inicia sesión con GitHub para dejar un comentario