Docker guarda todo lo que un contenedor escribe en su salida estándar (stdout) y de error (stderr), y te lo devuelve con docker logs. En este tutorial aprenderás a consultar esos logs de forma eficaz, a evitar que llenen el disco con la rotación de logs y a seguir un método ordenado para depurar contenedores que no arrancan, se reinician en bucle o no llegan a la red. Los comandos son para Ubuntu 24.04 con Docker Engine instalado desde el repositorio oficial.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.
- Tu usuario en el grupo
docker, o anteponersudoa cada comandodocker.
Comprueba la versión de Docker y el driver de logs que usa el daemon:
docker version --format '{{.Server.Version}}'
docker info --format '{{.LoggingDriver}}'
28.5.1
json-file
El número de versión que veas será el que tengas instalado. Lo importante es la segunda línea: json-file es el driver por defecto.
Paso 1: Crear un contenedor de prueba
Para practicar necesitas un contenedor que genere logs. Arranca Nginx publicando el puerto 8080 solo en localhost:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:stable
Genera un par de peticiones, una válida y otra que devuelva 404, para tener algo que leer:
curl -s -o /dev/null http://127.0.0.1:8080/
curl -s -o /dev/null http://127.0.0.1:8080/no-existe
La imagen oficial de Nginx redirige su log de acceso a stdout y su log de errores a stderr, así que ambos aparecerán en docker logs. Si tu aplicación escribe sus logs en un archivo dentro del contenedor, Docker no los verá: configúrala para que escriba en stdout/stderr.
Paso 2: Leer los logs con docker logs
Muestra todo el log del contenedor:
docker logs web
En la salida verás los mensajes de arranque de Nginx y dos líneas de acceso, una con código 200 y otra con 404.
En un contenedor con semanas de actividad eso son miles de líneas. Estas opciones limitan la salida:
| Opción | Qué hace |
|---|---|
--tail 100 | Solo las últimas 100 líneas |
-f | Sigue el log en tiempo real (sal con Ctrl+C) |
-t | Añade la marca de tiempo de Docker a cada línea |
--since 30m | Líneas de los últimos 30 minutos (acepta s, m, h o una fecha RFC 3339) |
--until 2026-09-25T12:00:00 | Líneas anteriores a esa fecha |
Lo más habitual al investigar un problema es combinar varias:
docker logs -f -t --tail 50 web
Para buscar texto con grep recuerda que docker logs reproduce stderr como stderr. Redirígelo a stdout o grep no lo verá:
docker logs --since 1h web 2>&1 | grep -i ' 404 '
172.17.0.1 - - [25/Sep/2026:10:14:02 +0000] "GET /no-existe HTTP/1.1" 404 153 "-" "curl/8.5.0" "-"
Logs de un proyecto de Docker Compose
Con Compose no necesitas saber el nombre de cada contenedor. Desde el directorio del proyecto (donde está compose.yaml) usa:
docker compose logs -f --tail 50
Añade el nombre de uno o varios servicios al final para filtrar, por ejemplo docker compose logs -f api db. Cada línea va prefijada con el nombre del contenedor, lo que ayuda a seguir una petición entre servicios.
Dónde se guardan los logs
Con el driver json-file, cada contenedor tiene su archivo en /var/lib/docker/containers/<id>/<id>-json.log. Puedes ver la ruta exacta y su tamaño así:
sudo ls -lh "$(docker inspect --format '{{.LogPath}}' web)"
No edites ni vacíes ese archivo a mano mientras el contenedor está en marcha: para controlar su tamaño usa la rotación del paso siguiente.
Paso 3: Configurar la rotación de logs
Por defecto json-file no tiene límite de tamaño: un contenedor muy hablador puede llenar el disco del servidor. Configura límites para todos los contenedores en /etc/docker/daemon.json:
sudo nano /etc/docker/daemon.json
Si el archivo no existe, créalo con este contenido. Si ya existe, añade las claves respetando el JSON que tenga:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Con esto cada contenedor guarda como máximo tres archivos de 10 MB. Valida el JSON antes de reiniciar, porque un error de sintaxis impide que Docker arranque:
sudo dockerd --validate --config-file /etc/docker/daemon.json
configuration OK
Reinicia el daemon para aplicar el cambio:
sudo systemctl restart docker
Importantela configuración de logs solo se aplica a contenedores creados después del cambio. Los existentes conservan la que tenían. Recréalos con
docker rm -fydocker run, o condocker compose up -d --force-recreateen proyectos de Compose.
Recrea el contenedor de prueba y comprueba que ha recibido los límites:
docker rm -f web
docker run -d --name web -p 127.0.0.1:8080:80 nginx:stable
docker inspect --format '{{json .HostConfig.LogConfig}}' web
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
Si prefieres fijar los límites por servicio en Compose, usa el bloque logging:
services:
web:
image: nginx:stable
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
NotaDocker también ofrece el driver
local, que rota por defecto y guarda los logs comprimidos, y el driverjournald, que envía los logs al journal de systemd (consultables conjournalctl CONTAINER_NAME=web). Ambos siguen funcionando condocker logs. Para enviar logs a un sistema centralizado (syslog remoto, Fluentd, Loki) consulta la documentación del driver correspondiente.
Paso 4: Averiguar por qué un contenedor se ha parado
Cuando un contenedor no está en marcha, empieza siempre por su estado. docker ps -a muestra también los contenedores parados y cómo terminaron:
docker ps -a --filter name=web
Para simular un fallo, lanza un contenedor cuyo proceso termina con error:
docker run --name falla alpine:3 sh -c 'echo "no encuentro la configuración" >&2; exit 3'
Los logs de un contenedor parado siguen disponibles hasta que lo borras:
docker logs falla
no encuentro la configuración
Consulta el estado completo, incluyendo el código de salida y si el kernel lo mató por falta de memoria:
docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' falla
status=exited exit=3 oom=false restarts=0
Los códigos de salida más útiles para diagnosticar:
| Código | Significado habitual |
|---|---|
0 | El proceso terminó correctamente. Si el contenedor debería seguir vivo, su comando no es un servicio en primer plano |
1 o propio de la app | Error de la aplicación: la causa está en docker logs |
125 | Docker no pudo crear el contenedor (opción de docker run incorrecta) |
126 | El comando existe pero no se puede ejecutar (permisos, formato) |
127 | El comando no existe en la imagen |
137 | Recibió SIGKILL: por falta de memoria (OOMKilled=true) o por docker kill |
143 | Recibió SIGTERM, normalmente por un docker stop |
Si el contenedor tiene una política de reinicio (--restart unless-stopped), un fallo se convierte en un bucle: docker ps mostrará Restarting (1) 5 seconds ago y RestartCount irá subiendo. En ese caso lee los logs anteriores al último reinicio con docker logs --tail 100 nombre.
Borra el contenedor de prueba:
docker rm falla
Paso 5: Inspeccionar un contenedor en ejecución
Cuando el contenedor funciona pero se comporta mal, entra en él. Abre una shell (usa sh si la imagen no incluye bash):
docker exec -it web sh
Algunas comprobaciones útiles sin entrar en la shell:
docker exec web env
docker exec web cat /etc/nginx/conf.d/default.conf
docker top web
docker top lista los procesos del contenedor vistos desde el host. Para ver el consumo de CPU, memoria, red y disco de todos los contenedores en una sola captura:
docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
3f1c2a9d7b21 web 0.00% 7.9MiB / 3.82GiB 0.20% 1.2kB / 0B 0B / 8kB 3
Si la memoria está cerca del límite y el contenedor acaba con OOMKilled=true, sube el límite con --memory o reduce el consumo de la aplicación.
Depurar la red de un contenedor
Muchas imágenes no incluyen ping, curl ni dig. En lugar de instalarlos dentro, arranca un contenedor de herramientas que comparta el espacio de red del contenedor a depurar. La imagen nicolaka/netshoot trae las habituales:
docker run --rm -it --network container:web nicolaka/netshoot
Dentro de ese contenedor, localhost es el propio contenedor web, así que puedes probar su puerto, su DNS y su salida a Internet:
curl -sI http://localhost:80
dig +short cubepath.com
Para conocer la IP de un contenedor en cualquier red (la plantilla funciona también con redes definidas por el usuario):
docker inspect --format '{{range $net, $cfg := .NetworkSettings.Networks}}{{$net}} {{$cfg.IPAddress}}{{"\n"}}{{end}}' web
bridge 172.17.0.2
Recuerda que los contenedores de una misma red definida por el usuario (por ejemplo, la que crea Compose) se resuelven por nombre de servicio; en la red bridge por defecto no hay resolución por nombre.
Paso 6: Revisar eventos y logs del daemon
Si el problema no está en la aplicación sino en Docker (el contenedor no se crea, un volumen no se monta, la red falla), mira el daemon. docker events muestra en tiempo real lo que ocurre: creación, arranque, muertes, OOM, health checks:
docker events --since 30m --until "$(date +%s)" --filter type=container
--until con la hora actual hace que el comando devuelva el histórico y termine en lugar de quedarse esperando. Añade --filter container=web para centrarte en un contenedor.
Los logs del propio daemon están en el journal de systemd:
sudo journalctl -u docker.service --since "1 hour ago" --no-pager
Si necesitas más detalle, añade "debug": true a /etc/docker/daemon.json y recarga la configuración sin parar los contenedores:
sudo systemctl reload docker
Desactívalo al terminar, porque el modo debug genera mucho volumen.
Solución de problemas
Bind for 0.0.0.0:8080 failed: port is already allocated. Otro proceso o contenedor usa ese puerto. Búscalo:
sudo ss -ltnp 'sport = :8080'
docker ps --filter publish=8080
Para el servicio que lo ocupa o publica el contenedor en otro puerto (-p 8081:80).
El contenedor sale nada más arrancar con código 0. El comando de la imagen termina en lugar de quedarse en primer plano (un script que lanza un demonio en segundo plano, o una imagen base como ubuntu sin comando). Revisa el CMD con docker inspect --format '{{.Config.Cmd}}' nombre y haz que el proceso principal se quede en primer plano.
Quiero ver qué hay dentro de una imagen que no arranca. Sustituye el punto de entrada por una shell para explorar el sistema de archivos:
docker run --rm -it --entrypoint sh nombre_imagen
permission denied al escribir en un volumen. El usuario del contenedor no tiene permisos sobre el directorio montado. Averigua con qué UID se ejecuta (docker exec nombre id) y ajusta el propietario del directorio en el host con sudo chown a ese UID. No uses chmod 777.
No hay espacio en disco. Mira qué ocupa Docker y limpia lo que no se use:
docker system df
docker image prune
docker builder prune
docker system prune -a --volumes borra también imágenes no usadas y volúmenes sin contenedor: úsalo solo si sabes que esos volúmenes no contienen datos que necesites. Si lo que crece son los logs, aplica la rotación del paso 3.
El contenedor no resuelve nombres DNS. Comprueba su /etc/resolv.conf con docker exec nombre cat /etc/resolv.conf y prueba la resolución desde netshoot. Si el servidor usa un resolver local en 127.0.0.53 (systemd-resolved), Docker ya lo sustituye por los servidores reales; si aun así falla, fija DNS explícitos con la clave "dns": ["1.1.1.1", "9.9.9.9"] en daemon.json.
Conclusión
Ya sabes leer y filtrar los logs de tus contenedores, limitar su tamaño para que no llenen el disco y seguir un orden de diagnóstico: estado y código de salida, logs, inspección en vivo, red y, por último, el daemon. Con eso se resuelven la mayoría de incidencias del día a día.
Como siguientes pasos puedes:
- Añadir una instrucción
HEALTHCHECKa tus imágenes para quedocker psmuestre si la aplicación responde, no solo si el proceso vive. - Enviar los logs a un sistema centralizado (Loki, Graylog o un syslog remoto) cuando gestiones varios servidores.
- Aplicar límites de memoria y CPU a tus contenedores para que un fallo no afecte al resto del servidor.
