Solución de problemas de contenedores Docker: problemas comunes de inicio y soluciones
Diagnostica contenedores Docker que se cierran, no pueden enlazar puertos, faltan archivos, tienen problemas de permisos o son eliminados por falta de memoria.
Solución de problemas de contenedores Docker: problemas comunes de inicio y soluciones
Cuando un contenedor Docker no se inicia, la solución más rápida suele venir de resistir la tentación de adivinar. Un contenedor es solo un proceso con un sistema de archivos, entorno, configuración de red y límites a su alrededor. Si ese proceso termina, Docker registra por qué. Tu trabajo es obtener la evidencia en el orden correcto.
Normalmente empiezo con tres preguntas: ¿Docker creó el contenedor?, ¿se inició el proceso principal?, ¿algo externo al proceso lo mató o bloqueó? Esas preguntas separan un nombre de imagen incorrecto de un comando roto, un conflicto de puerto de un fallo de aplicación, y un problema de permisos de un límite de memoria.
Empieza con el comando aburrido que te dice la verdad:
docker ps -a
Mira STATUS, PORTS y NAMES. Created significa que Docker creó el contenedor pero no lo puso en marcha realmente. Exited (1) a menudo significa que la aplicación devolvió un error normal. Exited (127) suele indicar un comando faltante. Exited (137) a menudo significa que el proceso fue eliminado desde fuera, frecuentemente por presión de memoria. Estos códigos son pistas, no respuestas definitivas, pero te evitan depurar la capa equivocada.
Luego lee los registros:
docker logs --tail 100 <contenedor>
docker logs -f <contenedor>
Si el contenedor muere inmediatamente, docker logs suele ser más útil que volver a ejecutar el mismo comando docker run. Los frameworks de aplicación a menudo imprimen la variable de entorno faltante exacta, el fallo de migración, el archivo de configuración inválido o el error de enlace antes de salir.
Para el estado de bajo nivel, inspecciona el contenedor:
docker inspect <contenedor> --format '{{json .State}}'
docker inspect <contenedor> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'
Vale la pena memorizar ese segundo comando. Te dice si Docker vio una muerte por OOM, qué código de salida se registró y si el propio runtime tuvo un error.
Si el contenedor se cierra inmediatamente
Un contenedor permanece vivo solo mientras su proceso principal permanece vivo. Si el comando termina, Docker detiene el contenedor. Eso sorprende a la gente cuando ejecutan scripts que inician un demonio en segundo plano y luego regresan.
Por ejemplo, este patrón a menudo termina:
CMD service nginx start
El comando service puede iniciar nginx y luego finalizar. Docker ve que el proceso principal termina y detiene el contenedor. El patrón amigable con contenedores es ejecutar el servidor en primer plano:
CMD ["nginx", "-g", "daemon off;"]
La misma idea aplica para procesos Node, Python, Java y trabajadores. El comando en CMD o ENTRYPOINT debe ser el proceso de larga duración, no un lanzador que pone el trabajo real en segundo plano y termina.
Si los registros muestran command not found, no such file or directory o exec format error, prueba la imagen de forma interactiva:
docker run --rm -it --entrypoint sh <imagen>
Algunas imágenes no incluyen bash, especialmente las imágenes Alpine y de estilo distroless. Usa sh primero a menos que sepas que bash existe. Una vez dentro, verifica la ruta del archivo, los permisos y el intérprete:
ls -l /app
which python || true
head -1 /app/start.sh
Un script puede existir y aún así fallar con no such file or directory si su shebang apunta a un intérprete faltante, como #!/bin/bash en una imagen que solo tiene /bin/sh. Otra causa común son los finales de línea de Windows. Si un script de shell se editó en Windows, el \r invisible puede hacer que Linux busque /bin/sh\r.
Si Docker dice que el puerto ya está asignado
Los conflictos de puerto ocurren en el lado del host. En -p 8080:80, 8080 es el puerto del host y 80 es el puerto del contenedor. Si algo ya está escuchando en el puerto 8080 del host, Docker no puede enlazarlo.
Puedes ver un error como bind: address already in use o port is already allocated. Encuentra el listener:
sudo lsof -i :8080
# o
sudo ss -ltnp 'sport = :8080'
En macOS, lsof suele ser lo más fácil. En servidores Linux, ss suele estar disponible por defecto. En PowerShell de Windows, usa:
Get-NetTCPConnection -LocalPort 8080
Luego elige un puerto host diferente o detén el servicio que lo posee:
docker run -d -p 8081:80 nginx
No cambies el puerto del contenedor a menos que la aplicación dentro del contenedor realmente escuche en ese nuevo puerto. Si nginx escucha en el puerto 80 dentro del contenedor, -p 8081:80 es correcto. -p 8081:8081 fallará desde el navegador si nada dentro del contenedor está escuchando en el 8081.
Si la aplicación se inicia pero no encuentra la configuración
Muchos fallos de inicio se deben a variables de entorno faltantes. La imagen está bien, el comando está bien, pero la aplicación espera DATABASE_URL, REDIS_URL, una clave API o un archivo de configuración.
Verifica lo que Docker pasó:
docker inspect <contenedor> --format '{{range .Config.Env}}{{println .}}{{end}}'
Para proyectos de Compose, inspecciona la configuración resuelta en lugar de solo leer docker-compose.yml:
docker compose config
Esto detecta errores de sangría, sorpresas del archivo .env y variables que se expandieron a cadenas vacías. Un ejemplo real: DATABASE_URL=${DATABASE_URL} parece inofensivo, pero si el shell o el archivo .env no lo define, tu aplicación puede recibir un valor vacío y fallar durante el inicio.
Ten cuidado con los secretos en los registros y el historial del terminal. Para depuración local rápida, pasar -e NOMBRE=valor está bien. Para sistemas compartidos, usa el mecanismo de secretos de tu plataforma o un archivo de entorno con permisos controlados.
Si los bind mounts o volúmenes causan errores de permiso
Un contenedor puede fallar al inicio porque no puede leer un archivo de configuración, escribir un archivo PID, crear un directorio de caché o inicializar un directorio de base de datos. Los registros suelen decir permission denied, read-only file system o operation not permitted.
Primero inspecciona el montaje:
docker inspect <contenedor> --format '{{json .Mounts}}'
Luego verifica qué usuario ejecuta el contenedor:
docker inspect <contenedor> --format 'user={{.Config.User}}'
Si user está vacío, la imagen puede ejecutarse como root por defecto, pero muchas imágenes de producción establecen un usuario no root. Un directorio del host propiedad de tu UID local puede no ser escribible por el UID 1000, 1001 o un usuario específico del servicio dentro del contenedor.
Una secuencia práctica de depuración es:
ls -ld ./data
docker run --rm -it -v "$PWD/data:/data" --entrypoint sh <imagen>
id
ls -ld /data
touch /data/test
Evita resolver cada problema de permisos con chmod 777. Puede ocultar el problema inmediato mientras crea uno peor. Prefiere igualar la propiedad o usar volúmenes con nombre para los datos de la aplicación:
docker volume create app_data
docker run -d -v app_data:/var/lib/app <imagen>
Los volúmenes con nombre son especialmente útiles en Docker Desktop, donde los bind mounts cruzan un límite de virtualización y pueden comportarse de manera diferente a los sistemas de archivos nativos de Linux.
Si el contenedor fue eliminado por memoria
El código de salida 137 es una fuerte indicación de que el proceso recibió SIGKILL. En el trabajo con Docker, eso a menudo significa que el kernel o Docker Desktop lo eliminó porque se agotó la memoria. Confírmalo con inspect:
docker inspect <contenedor> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'
Si OOMKilled es true, tienes dos tareas: darle al proceso suficiente memoria para iniciar y entender por qué necesitaba tanta. Aumentar el límite puede ser la solución de producción correcta para un servicio de base de datos o JVM. Para un servicio web pequeño, puede revelar un valor predeterminado incorrecto.
Las aplicaciones Java son un ejemplo clásico. El comportamiento antiguo de la JVM no siempre se ajustaba bien a los límites del contenedor, e incluso las JVM modernas aún necesitan configuraciones sensatas de -Xmx o basadas en porcentajes para un comportamiento predecible. Los servicios Node pueden necesitar --max-old-space-size en entornos con memoria limitada. Las bases de datos pueden necesitar configuraciones explícitas de caché.
Para una prueba puntual:
docker run --memory=1g <imagen>
Si usas Docker Desktop, también verifica la memoria asignada a la VM de Docker. Un límite de contenedor no puede ayudar si la VM misma está escasa.
Si la imagen nunca se descarga o la compilación nunca produjo una imagen
A veces no hay problema de contenedor porque no hay una imagen utilizable. Si docker run falla antes de crear un contenedor, verifica la imagen por separado:
docker image ls | grep mi-app
docker pull mi-registro/mi-app:etiqueta
Para registros privados, confirma la autenticación:
docker login <registro>
Para imágenes locales, asegúrate de que la etiqueta que ejecutas sea la etiqueta que compilaste:
docker build -t mi-app:dev .
docker run --rm mi-app:dev
Un error local común es compilar mi-app:dev y ejecutar mi-app:latest, que puede apuntar a una imagen más antigua o a nada.
Si se culpa a la red pero el servicio no está escuchando
Cuando un navegador no puede alcanzar un contenedor, la gente a menudo salta a la red de Docker. Primero demuestra que la aplicación está escuchando dentro del contenedor.
docker exec -it <contenedor> sh
ss -ltnp || netstat -ltnp
Si la aplicación está vinculada a 127.0.0.1 dentro del contenedor, la publicación de puertos de Docker no ayudará. La aplicación debe escuchar en 0.0.0.0 o en la dirección de interfaz del contenedor. Esto es común con servidores de desarrollo. Por ejemplo, muchos frameworks usan localhost por defecto y necesitan una bandera como --host 0.0.0.0.
Luego confirma el puerto publicado:
docker port <contenedor>
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Quieres ver algo como 0.0.0.0:8080->3000/tcp. Si no hay puerto publicado, el servicio puede funcionar desde otro contenedor en la misma red pero no desde el navegador de tu host.
Una lista de verificación de inicio confiable
Usa este orden cuando estés atascado:
docker ps -apara ver si el contenedor existe y cómo terminó.docker logs --tail 100 <contenedor>para leer la queja de la aplicación.docker inspect <contenedor>para verificar el código de salida, estado OOM, comando, usuario, montajes y puertos.docker run --rm -it --entrypoint sh <imagen>para probar la imagen manualmente.- Elimina una variable a la vez: primero ejecuta sin montajes, luego sin redes personalizadas, luego solo con las variables de entorno requeridas.
Ese último paso es importante. Un comando docker run largo con puertos, volúmenes, archivos de entorno, DNS personalizado, límites de memoria y un entrypoint personalizado te da demasiados sospechosos. Redúcelo hasta que la imagen se inicie, luego agrega configuraciones hasta que se rompa. La configuración que acabas de agregar suele ser donde reside el problema real.