Tutoriales de VPS
Contenedor Docker en VPS que se reinicia constantemente (2026): lista de verificación de logs, códigos de salida, health checks y OOM
Cuando un contenedor Docker en un VPS se reinicia constantemente, normalmente no es Docker en sí lo que está roto, sino que la aplicación falla al iniciar, falta configuración, hay conflictos de puertos, fallan los health checks o hay OOM. Diagnosticar en el orden de logs, códigos de salida, política de restart, recursos y dependencias permite localizar la causa rápidamente.

Contenedor Docker en VPS que se reinicia constantemente (2026): lista de verificación de logs, códigos de salida, health checks y OOM
El escenario que más he visto cuando un contenedor Docker está constantemente Restarting es este:
docker ps
En la salida, algún servicio se convierte repetidamente en:
Restarting (1) 12 seconds ago
Restarting (137) 8 seconds ago
Restarting ( unhealthy )
En este momento la primera reacción de muchas personas es reiniciar Docker:
systemctl restart docker
A veces esto sí alivia temporalmente, pero la mayoría de las veces no sirve. Porque un contenedor que se reinicia constantemente normalmente no es que el motor de Docker esté roto, sino que el proceso principal dentro del contenedor sale inmediatamente después de iniciar.
Es decir, Docker solo lo está levantando una y otra vez según tu política restart. El problema real suele estar oculto en:
- Errores de inicio de la aplicación
- Variables de entorno faltantes
- Rutas de archivos de configuración incorrectas
- Puerto ocupado
- Base de datos no conectada
- Permisos de archivos incorrectos
- Memoria insuficiente, terminado por OOM
- healthcheck mal escrito
Este artículo sigue el orden que suelo usar al diagnosticar, sin palabras vacías, directamente comandos y métodos de juicio.
El primer comando no es restart, sino:
docker ps -a
Asegúrate de añadir -a. De lo contrario, puede que no veas los contenedores que ya salieron.
Fíjate en estas columnas:
STATUS
NAMES
COMMAND
Los estados comunes son:
| Estado | Significado aproximado |
|---|---|
Exited (1) | La aplicación misma dio error y salió, el más común |
Exited (0) | El proceso principal terminó normalmente, pero no es adecuado como servicio permanente |
Exited (137) | Muy probablemente terminado por OOM o recibió SIGKILL |
Restarting (1) | Levantado repetidamente por la política de restart tras fallar el inicio |
Restarting (137) | Terminado después de cada inicio, revisa primero la memoria |
Si ves Restarting (1), no adivines, mira directamente los logs.
El más usado es:
docker logs container_name
Pero cuando el contenedor se reinicia constantemente, los logs pueden llenarse muy rápido. Recomiendo mirarlo así primero:
docker logs --tail=200 container_name
Si quieres observar de forma continua:
docker logs -f --tail=100 container_name
Si usas Docker Compose:
docker compose logs --tail=200 service_name
O mira todos los servicios:
docker compose logs --tail=200
Muchos problemas se escriben directamente en los logs:
Error: Cannot find module
permission denied
connect ECONNREFUSED 172.18.0.3:5432
address already in use
missing required environment variable DATABASE_URL
no such file or directory
Si no entiendes los logs, al menos encuentra el primer error. No mires solo la última línea, porque la última línea a menudo es solo el mensaje de salida del framework; la causa real está en las decenas de líneas anteriores.
Si aún no estás familiarizado con el diagnóstico de logs del servidor, puedes leer este artículo: Cómo leer los logs de un VPS: journalctl, systemctl, Nginx y logs de aplicaciones.
Los logs no siempre están completos, especialmente cuando el proceso es terminado por el sistema.
En este caso mira inspect:
docker inspect container_name --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
Posible salida:
137 true
Esto es muy claro: el contenedor fue terminado por OOM.
También puedes ver el State completo:
docker inspect container_name --format '{{json .State}}'
Si quieres un formato más legible:
docker inspect container_name | less
Campos clave:
ExitCode
OOMKilled
StartedAt
FinishedAt
Error
Health
Algunos códigos de salida comunes se pueden entender así:
| ExitCode | Significado común |
|---|---|
0 | El programa terminó normalmente, pero el contenedor no debería terminar |
1 | La aplicación falló al iniciar, error de configuración, fallo de dependencias |
126 | El comando no se puede ejecutar, problema de permisos o del script de entrada |
127 | El comando no existe, imagen o command mal escrito |
137 | Terminado por SIGKILL, común en OOM |
143 | Recibió SIGTERM, posiblemente una parada normal |
Para ExitCode=1, no lo atribuyas directamente a Docker, mira primero los logs de la aplicación. Para ExitCode=137, revisa primero la memoria.
Muchos archivos Compose contienen:
restart: always
O:
restart: unless-stopped
Esto no es malo, los entornos de producción normalmente lo necesitan. Pero puede hacerte pensar erróneamente que "el contenedor se rompe y se levanta solo una y otra vez".
Primero mira la política de restart:
docker inspect container_name --format '{{.HostConfig.RestartPolicy.Name}}'
Si estás diagnosticando, puedes detener temporalmente el bucle de reinicio:
docker update --restart=no container_name
docker stop container_name
Luego inícialo manualmente una vez:
docker start container_name
docker logs --tail=200 container_name
Así los logs serán mucho más claros y no se llenarán constantemente.
Si es un servicio gestionado por Compose, cambia docker-compose.yml y luego:
docker compose up -d
No cambies manualmente demasiados parámetros de ejecución del contenedor, porque al final será fácil que quede inconsistente con el archivo Compose.
El problema de reinicio de Docker que más he encontrado es la primera categoría: variables de entorno no pasadas.
Por ejemplo, la aplicación necesita:
DATABASE_URL
REDIS_URL
SECRET_KEY
JWT_SECRET
APP_URL
Pero .env no está en el lugar correcto, o Compose no lo referencia.
Primero mira qué variables de entorno recibió realmente el contenedor:
docker inspect container_name --format '{{range .Config.Env}}{{println .}}{{end}}'
Si usas Compose, confirma que el archivo .env está en el directorio donde ejecutas docker compose.
Errores comunes:
environment:
DATABASE_URL: ${DATABASE_URL}
Pero el shell actual o .env no tiene DATABASE_URL en absoluto.
Puedes ver primero la configuración renderizada de Compose:
docker compose config
Este comando es muy útil, te ayuda a ver si las variables finalmente fueron sustituidas.
Si la salida sigue mostrando valores vacíos:
DATABASE_URL: ""
Entonces no es de extrañar que el contenedor se reinicie.
Hay dos tipos de conflictos de puertos.
Por ejemplo, Compose dice:
ports:
- "80:80"
Pero en el host Nginx ya ocupa el 80.
Consulta el puerto:
ss -lntp | grep ':80'
O:
lsof -i :80
Si es Docker mismo el que lo ocupa, también puedes ver:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Algunas aplicaciones escuchan por defecto en 3000, pero tú crees que escuchan en 80.
Por ejemplo:
ports:
- "8080:80"
Pero la aplicación realmente corre en 3000 dentro del contenedor, entonces debería ser:
ports:
- "8080:3000"
Para problemas de puertos puedes continuar con este artículo: Qué hacer cuando un puerto del VPS está ocupado.
Cuando la aplicación dentro del contenedor inicia, necesita leer configuración, escribir caché y escribir logs, pero si los permisos del directorio montado del host son incorrectos, también saldrá directamente.
Por ejemplo, en los logs aparece:
permission denied
read-only file system
cannot open config file
no such file or directory
Primero mira los montajes:
docker inspect container_name --format '{{json .Mounts}}'
Luego mira el directorio del host:
ls -la /path/on/host
Errores comunes:
- El directorio del host no existe y Docker creó automáticamente un directorio vacío
- El owner del archivo es
root, pero la aplicación dentro del contenedor se ejecuta con el usuarionode,www-dataoapp - El archivo de configuración se montó como directorio
- El directorio se montó en la ubicación incorrecta, sobrescribiendo los archivos originales de la imagen
Por ejemplo, originalmente querías montar un archivo:
volumes:
- ./config.yml:/app/config.yml
Pero el config.yml local no existe, y en algunos casos se convierte en un directorio, por lo que la aplicación naturalmente no puede leerlo.
Si encuentras problemas de permisos de Docker, puedes leer este artículo: Qué hacer con Docker Permission Denied en un VPS.
Muchas aplicaciones necesitan conectarse a una base de datos o Redis al iniciar.
Crees que Compose ya tiene:
depends_on:
- db
Y todo está resuelto. En realidad no.
depends_on por defecto solo garantiza el orden de inicio, no que la base de datos esté lista.
Así que la aplicación puede conectarse a la base de datos en cuanto inicia:
connect ECONNREFUSED db:5432
Y luego salir directamente, Docker la levanta de nuevo, formando un bucle de reinicio.
Un enfoque más robusto es:
- La aplicación misma soporta reintentos de conexión
- Añadir healthcheck a la base de datos
- El script de inicio de la aplicación espera a que la base de datos esté lista
Por ejemplo, PostgreSQL:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthy
Si quieres usar Compose en producción, recomiendo leer este artículo: Cómo configurar Docker Compose para producción en un VPS.
A veces la aplicación realmente inició, pero el contenedor sigue reiniciándose.
En este caso hay que ver si el healthcheck y la política de restart están interactuando mal.
Primero mira el estado de salud:
docker inspect container_name --format '{{json .State.Health}}'
O:
docker ps
Si ves:
Up 30 seconds (unhealthy)
Significa que el proceso principal sigue vivo, pero el health check falla.
Errores comunes:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
Pero la imagen no tiene curl en absoluto.
O la ruta del health check de la aplicación no es /health, sino /api/health.
Otro caso: la aplicación inicia muy lento, pero el healthcheck es demasiado impaciente:
interval: 5s
timeout: 2s
retries: 3
La aplicación aún no ha arrancado y ya se juzga como fallida.
Puedes añadir start_period:
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
Si accedes a través de un proxy inverso, ten en cuenta que el healthcheck se ejecuta dentro del contenedor, y localhost se refiere al propio contenedor, no al host, ni al contenedor de Nginx.
Si el código de salida es 137, o en inspect ves:
OOMKilled: true
Revisa primero la memoria.
Mira los logs del sistema:
dmesg -T | grep -i 'killed process\|out of memory\|oom'
Mira los recursos del contenedor:
docker stats
Mira si Compose lo limita demasiado:
mem_limit: 256m
Muchas aplicaciones Java, Node y Python consumen un pico de memoria en el momento del inicio. Le das 256MB, parece suficiente para correr, pero en la fase de inicio ya es terminado.
Enfoque de solución:
- Dar al contenedor un límite de memoria más razonable
- Añadir swap o zram al VPS
- Reducir la concurrencia y el número de workers de la aplicación
- Comprobar si hay fugas de memoria
- Separar servicios pesados como bases de datos y servicios de búsqueda
Para VPS con poca memoria puedes leer este artículo: Cómo optimizar un VPS de poca memoria con ZRAM y Swap. Si ya hay OOM frecuentes, también puedes leer: ¿VPS sin memoria suficiente? Soluciones al problema de OOM.
Cuando el disco está lleno, el contenedor puede no reportar inmediatamente "disco lleno", sino manifestarse como:
- La base de datos falla al iniciar
- La aplicación falla al escribir caché
- Los logs no se pueden escribir
- Anomalías en overlay2
- El contenedor sale justo después de iniciar
Primero mira el disco:
df -h
Luego mira los inodos:
df -i
Luego mira el uso de Docker:
docker system df
No ejecutes esto de inmediato:
docker system prune -a
Este comando puede borrar imágenes y caché de compilación que aún necesitas. Mira bien antes de limpiar.
Para una ruta de limpieza más robusta puedes leer este artículo: Qué hacer cuando Docker llena el disco en un VPS.
Algunos contenedores Exited (0), no por fallo, sino porque el comando terminó de ejecutarse.
Por ejemplo, escribiste:
command: npm run build
Después de completar la compilación, el proceso sale y Docker considera que el contenedor terminó. Si además escribiste:
restart: always
Se reiniciará, saldrá y volverá a compilar continuamente.
Un servicio permanente debería ser:
command: npm run start
O el Dockerfile de la imagen debe tener el correcto:
CMD ["node", "server.js"]
También hay que prestar atención al problema de primer plano/fondo. El proceso principal del contenedor debe ejecutarse en primer plano. Si pones el servicio en segundo plano, el proceso principal termina y el contenedor también termina.
Ejemplo incorrecto:
nginx &
Enfoque correcto: mantener el proceso principal en primer plano.
Después de cambiar .env, docker-compose.yml o las rutas de montaje, solo docker restart normalmente no es suficiente.
Lo más robusto es:
docker compose up -d --force-recreate
Si también cambió la imagen:
docker compose up -d --build --force-recreate
Si solo cambiaste un archivo de configuración montado desde el host, mira si la aplicación soporta recarga en caliente. Si no lo soporta, aún necesitas reiniciar el contenedor correspondiente:
docker compose restart app
Pero si cambiaste variables de entorno, puertos o mapeos de volume, normalmente hay que recrear el contenedor.
Normalmente sigo este orden:
# 1. 看所有容器状态
docker ps -a
# 2. 看最近日志
docker logs --tail=200 container_name
# 3. 看退出码、OOM、错误状态
docker inspect container_name --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
# 4. 看 restart 策略
docker inspect container_name --format '{{.HostConfig.RestartPolicy.Name}}'
# 5. 临时停止重启循环
docker update --restart=no container_name
# 6. 手动启动一次,看第一条错误
docker start container_name
docker logs --tail=200 container_name
Si es Compose:
docker compose ps -a
docker compose logs --tail=200 service_name
docker compose config
docker compose up -d --force-recreate
Luego deriva según el tipo de error:
| Fenómeno | Qué revisar primero |
|---|---|
Exited (1) | Logs de la aplicación, variables de entorno, archivos de configuración, servicios de dependencia |
Exited (127) | Si command / entrypoint está mal escrito |
Exited (137) | OOM, límites de memoria, logs del sistema |
permission denied | Permisos de volume, usuario, owner de archivos |
address already in use | Conflicto de puerto del host o del puerto interno del contenedor |
connect refused | Si la base de datos/Redis está lista, si el nombre de red es correcto |
unhealthy | Comando, ruta y tiempo de inicio del healthcheck |
Que el contenedor pase de Restarting a Up es solo el primer paso.
También hay que seguir mirando:
docker ps
docker logs --tail=100 container_name
docker stats
Luego accede al servicio una vez desde fuera:
curl -I http://127.0.0.1:your_port
Si hay Nginx delante:
curl -I https://your-domain.com
Por último, recomiendo añadir esto:
- Una política
restartrazonable, no pongasalwaysa todos los servicios sin pensar - Añadir
start_periodal healthcheck - Límites de logs para evitar llenar el disco
- Añadir límites de recursos a servicios clave, pero no limitarlos demasiado
- Copias de seguridad periódicas de los volúmenes de datos importantes
Por ejemplo, límites de logs:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Para la configuración de Compose en producción puedes consultar: Cómo configurar Docker Compose para producción en un VPS.
Si esta vez es un contenedor Docker que se reinicia constantemente, lo más probable es que más adelante encuentres estos problemas:
- Cómo configurar Docker Compose para producción en un VPS
- Qué hacer cuando Docker llena el disco en un VPS
- Qué hacer con Docker Permission Denied en un VPS
- Qué hacer cuando un contenedor Docker no puede conectarse a la red en un VPS
Un último recordatorio: cuando encuentres Restarting, no te apresures a borrar el contenedor o el volume. Mira primero los logs, los códigos de salida y OOMKilled. Muchas veces lo que te falta no es reinstalar, sino encontrar el primer error real.