Solución de problemas: Diagnóstico rápido de errores comunes en contenedores Docker

Domina el arte de la solución rápida de problemas en contenedores Docker con esta guía esencial. Aprende el proceso estructurado para diagnosticar fallos de inicio utilizando comandos básicos de Docker. Detallamos cómo usar `docker ps -a` para identificar fallos, extraer información crítica con `docker logs` y realizar análisis avanzados de configuración con `docker inspect`. Este artículo proporciona ejemplos prácticos y soluciones específicas para problemas frecuentes, incluidos errores de código de salida 127, conflictos de puertos y eventos OOMKilled, asegurando que puedas identificar rápidamente la causa raíz y restaurar el servicio.

Solución de problemas: Diagnóstico rápido de errores comunes en contenedores Docker

Cuando un contenedor Docker sale inmediatamente, no empieces reconstruyendo la imagen o cambiando banderas aleatorias. Empieza por averiguar lo que Docker sabe: el estado del contenedor, el código de salida, los registros y el comando exacto que Docker intentó ejecutar. Esas cuatro piezas suelen acotar el problema rápidamente.

Un contenedor es solo un proceso con aislamiento a su alrededor. Si el proceso principal sale, el contenedor sale. Eso puede ser un fallo, un ejecutable faltante, un trabajo por lotes completado, una dependencia de salud fallida o el kernel matando el proceso porque usó demasiada memoria. Los comandos a continuación te ayudan a distinguir estos casos.

Encuentra primero el contenedor detenido

docker ps solo muestra contenedores en ejecución. Los contenedores con fallos de inicio suelen estar ocultos a menos que solicites todos los contenedores:

docker ps -a

Mira STATUS, COMMAND y NAMES:

CONTAINER ID   IMAGE          COMMAND              STATUS                      NAMES
2d3f4b5c6e7a   my-app:latest  "/usr/bin/start"     Exited (127) 2 minutes ago  web-service
91aa34c0db22   worker:latest  "python worker.py"   Exited (0) 10 minutes ago   nightly-worker

Exited (0) a menudo significa que el proceso se completó con éxito. Eso es normal para trabajos de una sola ejecución. Para un servicio web, puede significar que el comando se ejecutó y finalizó en lugar de permanecer en primer plano.

Los códigos de salida distintos de cero apuntan a fallos, pero trátalos como pistas en lugar de respuestas definitivas. El código de salida 127 comúnmente significa comando no encontrado. 126 comúnmente significa encontrado pero no ejecutable. 137 a menudo significa que el proceso recibió SIGKILL; en contenedores, eso frecuentemente, pero no siempre, está relacionado con la presión de memoria. Siempre confirma con registros y la salida de inspección.

Lee los registros antes de cambiar nada

Docker captura stdout y stderr del proceso principal del contenedor para el controlador de registro predeterminado. Usa:

docker logs web-service

Opciones útiles:

docker logs --tail 100 web-service
docker logs --since 15m web-service
docker logs -t web-service
docker logs -f web-service

Si los registros dicen config file not found, verifica los montajes y el entorno. Si muestran un seguimiento de pila de la aplicación, depura la aplicación. Si están vacíos, el proceso puede haber fallado antes de producir salida, o el punto de entrada de la imagen puede ser incorrecto.

Para un bucle de fallos, evita docker logs -f como tu única herramienta. Puede hacer que el fallo se sienta activo sin darte el estado. Combina los registros con docker inspect.

Inspecciona el estado del contenedor

docker inspect devuelve un documento JSON grande. Rara vez necesitas todo. Empieza con campos formateados:

docker inspect -f 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' web-service

Luego inspecciona el comando y la configuración de la imagen:

docker inspect -f 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{.Config.User}}' web-service

Verifica los montajes cuando el error involucra archivos:

docker inspect -f '{{json .Mounts}}' web-service

Si el contenedor fue eliminado por memoria, .State.OOMKilled es el campo importante. Si es true, aumentar la memoria puede ayudar, pero la mejor siguiente pregunta es por qué creció la memoria. Un límite más grande puede ocultar una fuga el tiempo suficiente para fallar más tarde.

Reproduce con un shell interactivo cuando sea posible

Si la imagen contiene un shell, anula el punto de entrada e inspecciona el sistema de archivos:

docker run --rm -it --entrypoint /bin/sh my-app:latest

Algunas imágenes tienen Bash:

docker run --rm -it --entrypoint /bin/bash my-app:latest

Dentro, verifica los archivos y las rutas de los comandos:

ls -l /usr/bin/start
id
env

Las imágenes mínimas pueden no incluir un shell. En ese caso, usa las herramientas disponibles de la imagen, reconstruye una variante de depuración temporal o inspecciona el Dockerfile y la salida de la compilación. No agregues permanentemente paquetes de depuración a una imagen de producción solo porque la solución de problemas fue inconveniente una vez.

Comando no encontrado: salida 127

La salida 127 generalmente significa que Docker no pudo encontrar el ejecutable nombrado por ENTRYPOINT o CMD, o un script de inicio intentó ejecutar un comando faltante.

Causas comunes:

  • El ejecutable nunca se copió en la imagen.
  • La ruta es correcta en el host pero no dentro de la imagen.
  • El script usa /bin/bash, pero la imagen solo tiene /bin/sh.
  • El comando depende de PATH, y PATH difiere de lo que esperas.

Verifica el comando de la imagen:

docker inspect -f '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' web-service

Si el punto de entrada es un script, verifica su shebang y finales de línea. Un script con finales de línea CRLF de Windows puede fallar con mensajes confusos de "no encontrado" porque la ruta del intérprete contiene efectivamente un retorno de carro.

Permiso denegado: salida 126 o errores de archivo

La salida 126 a menudo significa que Docker encontró el comando pero no pudo ejecutarlo. Para scripts, el archivo puede carecer del bit ejecutable:

COPY start.sh /usr/local/bin/start.sh
RUN chmod 0755 /usr/local/bin/start.sh
ENTRYPOINT ["/usr/local/bin/start.sh"]

Para archivos montados por volumen, recuerda que se aplican los permisos del host. Si un contenedor se ejecuta como UID 1000 y el directorio del host es propiedad de root sin permiso de escritura, el contenedor no puede escribir allí solo porque está "dentro de Docker".

Verifica el usuario en tiempo de ejecución:

docker inspect -f 'user={{.Config.User}}' web-service

Si está en blanco, muchas imágenes se ejecutan como root por defecto, pero no todas. Las imágenes oficiales y reforzadas en seguridad a menudo usan un usuario no root.

Puerto ya asignado

Un error de enlace generalmente aparece cuando publicas un puerto del host que ya está en uso:

docker run -p 8080:80 nginx

Docker puede informar algo como bind: address already in use. Encuentra el conflicto:

docker ps --format 'table {{.Names}}\t{{.Ports}}'
lsof -iTCP:8080 -sTCP:LISTEN

Luego detén el proceso conflictivo o elige otro puerto del host:

docker run -p 8081:80 nginx

El puerto del contenedor puede permanecer igual. El puerto del host es la parte antes de los dos puntos.

Archivos faltantes y montajes incorrectos

Si los registros dicen que falta un archivo de configuración, compara lo que la aplicación espera con lo que Docker montó:

docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' web-service

Un error común es montar un directorio del host sobre una ruta que ya tenía archivos en la imagen. El montaje oculta el contenido de la imagen en ese destino. Si la imagen contiene /app/config/default.yml y montas un directorio vacío del host sobre /app/config, el archivo predeterminado desaparece de la vista del contenedor.

También verifica las rutas relativas. -v ./config:/app/config depende del directorio donde ejecutaste docker run, no del directorio donde reside el Dockerfile.

Fallos en las comprobaciones de salud no siempre son fallos de contenedor

Un contenedor puede estar en ejecución pero no saludable:

docker ps

Puedes ver Up 2 minutes (unhealthy). Inspecciona la salida de salud:

docker inspect -f '{{json .State.Health}}' web-service

Las comprobaciones de salud a menudo fallan porque la aplicación está escuchando en un puerto diferente, se vincula solo a 127.0.0.1, tarda más en iniciar de lo que permite la comprobación de salud, o necesita una base de datos que aún no está lista. No confundas un contenedor no saludable con uno que ha salido; la ruta de diagnóstico es diferente.

Una secuencia rápida de solución de problemas

Usa este orden cuando necesites una respuesta rápidamente:

  1. docker ps -a para encontrar el contenedor y el código de salida.
  2. docker logs --tail 100 <nombre> para leer el error de la aplicación.
  3. docker inspect -f ... para verificar el estado, comando, usuario y montajes.
  4. Ejecuta un shell temporal en la imagen si el comando o el sistema de archivos es sospechoso.
  5. Verifica conflictos del host para puertos y permisos de directorios montados.
  6. Reconstruye solo después de saber si el problema es el contenido de la imagen, las banderas de tiempo de ejecución o la configuración de la aplicación.

Esa secuencia mantiene la investigación fundamentada. Docker generalmente tiene suficiente evidencia; el truco es leerla antes de cambiar la escena.

Verifica la política de reinicio antes de confiar en lo que ves

Una política de reinicio puede hacer que un contenedor parezca que está fallando constantemente o recuperándose constantemente. Verifícala:

docker inspect -f 'restart={{json .HostConfig.RestartPolicy}}' web-service

Si la política es always o unless-stopped, Docker puede reiniciar el contenedor después de cada fallo. docker ps podría mostrarlo como en ejecución durante unos segundos, luego reiniciándose de nuevo. En ese caso, usa registros con marcas de tiempo e inspecciona el recuento de reinicios:

docker inspect -f 'restarts={{.RestartCount}} started={{.State.StartedAt}} finished={{.State.FinishedAt}}' web-service

Un recuento alto de reinicios generalmente significa que el proceso principal sale rápidamente. La solución rara vez es "cambiar la política de reinicio". La política solo está revelando el fallo subyacente.

Distingue problemas de tiempo de compilación de problemas de tiempo de ejecución

Si falta un archivo dentro del contenedor, pregúntate cuándo debería haber aparecido. Los archivos copiados en el Dockerfile son preocupaciones de tiempo de compilación. Los archivos montados con -v o volúmenes de Compose son preocupaciones de tiempo de ejecución.

Verificaciones de tiempo de compilación:

docker image inspect my-app:latest
docker run --rm --entrypoint /bin/sh my-app:latest -c 'ls -la /app'

Verificaciones de tiempo de ejecución:

docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' web-service

Esta división ahorra tiempo. Reconstruir la imagen no solucionará un montaje incorrecto del host. Cambiar una bandera de volumen no solucionará un Dockerfile que nunca copió el binario.

Las variables de entorno y los secretos pueden fallar silenciosamente

Muchas aplicaciones salen porque falta una variable de entorno requerida, pero el error de Docker solo dice que el proceso salió con el código 1. Inspecciona cuidadosamente el entorno configurado:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web-service

Ten cuidado dónde ejecutas ese comando; puede imprimir secretos. En registros compartidos, imprime solo los nombres de las variables:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web-service | sed 's/=.*//'

Si usas --env-file, verifica los finales de línea CRLF, espacios sin comillas y archivos faltantes. Los archivos env de Docker no son scripts de shell completos. Mantenlos simples: líneas KEY=value, comentarios donde sean compatibles con tu versión de Docker, y sin suposiciones de que ocurrirá expansión de shell dentro del archivo.

Una revisión del mundo real antes de enviar

Antes de dar por terminado un script o la configuración de un contenedor, léelo una vez como si fueras la próxima persona que tiene que depurarlo a las 2 a.m. Eso cambia lo que notas. Un mensaje que tenía sentido mientras escribías el script puede ser ambiguo cuando aparece en un registro de CI. Un nombre de servicio de Docker que parecía obvio puede no coincidir con el nombre de la variable en la aplicación. Un valor predeterminado de Bash puede ser seguro para el desarrollo y peligroso para la producción.

Me gusta hacer una prueba en seco corta con valores deliberadamente incómodos. Usa una ruta con espacios. Usa un valor opcional vacío. Prueba un nombre de archivo que comience con un guión. Ejecuta el script desde un directorio de trabajo diferente. Inicia el contenedor sin una variable de entorno esperada. Estas pruebas no son sofisticadas, pero detectan las suposiciones que generalmente fallan primero.

También verifica el mensaje de fallo. Si la única salida es failed, el consejo del artículo no se ha implementado. Un fallo útil dice qué valor se usó, qué verificación falló y qué puede cambiar el operador. Eso no significa volcar cada variable de entorno o imprimir secretos. Significa ser específico donde la especificidad ayuda: la ruta de configuración, el nombre del comando faltante, el nombre de la red, el nombre de host del servicio o el puerto que el proceso intentó vincular.

El hábito final es mantener los ejemplos cerca de la forma en que realmente se ejecuta el sistema. Si la producción usa Compose, prueba con Compose. Si un script es lanzado por systemd, pruébalo con systemd o con un entorno igualmente mínimo. Si se supone que un comando es seguro para copiar y pegar, incluye las comillas, los separadores -- y la validación en el ejemplo mismo. Los lectores copian patrones de trabajo más a menudo de lo que copian advertencias.

Esa revisión no es burocracia. Es cómo la pequeña automatización se vuelve aburrida. Aburrido es lo que quieres de los mensajes de shell, los cargadores de configuración, la expansión de variables, el diagnóstico de contenedores y la red de Docker. Cuanto menos sorprendente sea el comportamiento, más fácil será para el próximo operador confiar en él.

Para fallos de Docker, guarda el comando exacto que creó el contenedor cuando puedas. docker inspect puede mostrar la configuración actual, pero una nota de incidente que incluya el comando docker run original, el servicio de Compose, la etiqueta de la imagen y el nombre del archivo env es mucho más fácil de reproducir más tarde. Evita usar latest durante la depuración seria. Una etiqueta móvil puede convertir un fallo en dos investigaciones diferentes porque la imagen cambia mientras aún estás leyendo los registros.

Si estás diagnosticando un problema similar al de producción localmente, extrae el mismo digest o etiqueta de imagen, usa el mismo diseño de volumen y pasa la misma forma de entorno no secreta. La reproducción supera a la especulación.