Contenedores huérfanos de Docker: por qué aparecen y cómo purgarlos de forma segura

¿Te fue útil?

Los contenedores huérfanos aparecen como el equipaje no reclamado en un aeropuerto: silenciosos, persistentes y siempre cuando ya vas tarde.
El disco se llena. Los puertos chocan. El monitoreo grita sobre contenedores “desconocidos”. Y lo peor: la mitad de las veces, el “huérfano” en realidad está haciendo algo útil.

Esta es la guía de campo para averiguar qué está realmente huérfano, qué está simplemente sin etiquetar y cómo limpiar sin convertir una ventana de mantenimiento rutinaria en un evento de carrera profesional.

Qué significa realmente “contenedor huérfano” (y lo que no significa)

“Contenedor huérfano” no es un concepto del runtime de Docker. Docker no tiene una marca que diga “este contenedor está huérfano”.
Huérfano es una etiqueta operativa que ponemos a contenedores que parecen desconectados de la herramienta y la intención que los creó.

En la práctica, un contenedor se llama “huérfano” cuando una (o más) de estas condiciones es verdadera:

  • La herramienta de despliegue ya no lo conoce (proyecto Compose renombrado, stack de Swarm eliminado, pipeline de CI movido, etc.).
  • Nadie puede nombrar un propietario (sin etiquetas, sin mapeo de servicio, sin ticket, sin runbook).
  • Sigue en ejecución, pero nadie lo está vigilando (sin métricas, sin envío de logs, sin alertas).
  • Está detenido pero no eliminado y se acumula lentamente con versiones antiguas.

La peligrosa idea equivocada es que “huérfano” significa “seguro para eliminar”. A veces lo es. A veces es tu única copia de un sidecar con estado
que alguien arrancó a mano a las 2 a.m. durante un incidente y luego se olvidó de documentar.

Si quieres una definición más fiable para producción: un huérfano es un contenedor cuyo controlador de ciclo de vida ha desaparecido.
Sin controlador no hay actualizaciones previsibles, ni gestión de deriva, ni limpieza automática. Por eso se acumulan.

Por qué aparecen contenedores huérfanos en sistemas reales

Los huérfanos no son una falta moral. Son una propiedad emergente de equipos que publican software más rápido de lo que mantienen higiene operativa.
Estas son las causas principales que veo repetidamente.

1) Deriva en nombre/proyecto de Docker Compose

Compose identifica “sus” contenedores por un nombre de proyecto. Cambia el nombre del directorio, usa -p a veces pero no siempre,
o actualiza versiones de Compose con valores por defecto ligeramente distintos, y de pronto tienes múltiples “proyectos” en el mismo host. Los contenedores de cada proyecto
se ven como desconocidos para los otros.

2) La estrategia de CI/CD deja contenedores antiguos

Blue/green o canary pueden ser limpias. O pueden ser “iniciar contenedores nuevos, olvidar detener los antiguos”.
Especialmente con scripts ad-hoc que hacen docker run con nuevas etiquetas de imagen y nunca eliminan el contenedor anterior.

3) Bucles de fallos crean cementerios de contenedores detenidos

Un contenedor que falla inmediatamente puede ser reiniciado por una política o un supervisor. Pero las instancias antiguas pueden permanecer si se crean repetidamente
por un pipeline, un script o ejecuciones de Compose con nombres cambiantes. Terminas con docenas o cientos de contenedores exited, cada uno con logs
y capas escribibles que consumen disco.

4) Ingenieros ejecutan “solo un contenedor rápido” en prod

Puedes sentir la forma del incidente: alguien necesita una migración puntual, una herramienta de depuración, una captura de paquetes, un backup. Ejecutan un contenedor.
Prometen que lo eliminarán. Entonces pasa Slack. Entonces llega el cuarto trimestre. Ahora es inmortal.

5) Swarm, Kubernetes o systemd cambiaron, pero los contenedores quedaron

Cuando migras capas de orquestación, hay un periodo de transición donde hosts antiguos siguen ejecutando cargas legacy.
Al eliminar el orquestador también se elimina la parte que limpiaba esos contenedores.

6) Redes y volúmenes sobreviven a los contenedores (por diseño)

Docker trata intencionadamente los volúmenes como duraderos. Eso es bueno. Pero también significa que la limpieza no es “eliminar contenedores” — es “eliminar contenedores,
redes, volúmenes e imágenes con el conjunto correcto de precauciones.” Fallar en uno y sigues perdiendo disco.

Un chiste, porque te lo mereces: la limpieza de Docker es como ordenar un garaje—todo es “temporal” hasta que se convierte en una pila que aguanta carga.

Datos interesantes y contexto histórico (lo que explica el desastre actual)

  1. Los volúmenes de Docker fueron diseñados para sobrevivir a los contenedores, por eso eliminar contenedores no libera tu uso “real” de disco.
  2. Docker Compose originalmente fue pensado para desarrollo local. El uso en producción se volvió común porque era conveniente, no porque fuera la herramienta ops perfecta.
  3. Los flujos de trabajo tempranos de Docker usaban “mascotas” (contenedores gestionados manualmente) mucho antes de que las prácticas de infraestructura inmutable se impusieran.
  4. Las etiquetas de Compose se convirtieron en el sistema de metadatos por defecto: Compose moderno marca contenedores con labels como com.docker.compose.project y com.docker.compose.service.
  5. Las “imágenes colgantes” son un efecto secundario de las builds en capas: cuando una etiqueta se mueve, las capas antiguas pueden quedar sin referencia y aún así ocupar espacio.
  6. El comportamiento del sistema de archivos overlay de Docker importa: las capas escribibles de los contenedores pueden crecer incluso si tu app escribe datos “temporales”, porque lo “temporal” puede estar dentro de la capa.
  7. Las políticas de reinicio pueden ocultar fallos: un contenedor que reinicia para siempre parece “saludable” en docker ps si solo miras el tiempo “Up”.
  8. Compose v2 se integró en la CLI de Docker (como docker compose), lo que cambió rutas de instalación y a veces comportamiento en flotas.
  9. Se añadieron comandos prune porque la gente llenaba discos constantemente. Son poderosos, afilados y fáciles de usar mal.

Guion de diagnóstico rápido

Cuando sospechas de contenedores huérfanos, normalmente respondes a uno de tres dolores: presión de disco, conflictos de puertos o “¿qué es esto?” durante un incidente.
Este guion te lleva al cuello de botella rápidamente sin eliminar lo equivocado.

Primero: identifica la categoría del problema en 60 segundos

  • Presión de disco: el filesystem raíz del host Docker se está llenando, o /var/lib/docker es enorme.
  • Conflicto de puertos: el despliegue falla porque el puerto ya está ligado, o el tráfico va al contenedor equivocado.
  • Runtime desconocido: el monitoreo muestra picos de CPU/ram por un contenedor que nadie reconoce.

Segundo: mapea contenedores al “controlador”

Tu objetivo: determinar si cada contenedor sospechoso está controlado por Compose, Swarm, Kubernetes, systemd o por el “historial de shell de alguien”.
Las etiquetas y patrones de nombres te dicen la mayor parte de lo que necesitas.

Tercero: decide “detener” versus “eliminar” versus “dejar”

Detener es reversible-ish (hasta que olvidas por qué existía). Eliminar es definitivo para la capa escribible, no necesariamente para los volúmenes.
Dejar es aceptable cuando no puedes probar que es seguro y necesitas tiempo para investigar.

Cuarto: limpia la clase de recurso correcta

Los contenedores no son la única fuga. Imágenes, cache de build, volúmenes y redes tienen sus propios modos de fallo.
Arregla al culpable más grande primero, y no toques volúmenes a menos que hayas verificado que no están en uso y no forman parte de un plan de restauración.

Una cita de fiabilidad que realmente aguanta: “La esperanza no es una estrategia.” — idea parafraseada que los ingenieros y operadores conocen bien.
El punto es simple: no confíes en “probablemente seguro eliminar”; verifica.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estos son los comandos que ejecuto en hosts reales. Cada tarea incluye: comando, qué significa la salida y la decisión que tomas a continuación.
Ejecútalos como usuario con privilegios Docker (a menudo root o en el grupo docker). En producción, prefiere hacerlo en una sesión screen/tmux
y registra tus acciones.

Tarea 1: Lista de contenedores en ejecución con columnas de alta señal

cr0x@server:~$ docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
CONTAINER ID   NAMES                      IMAGE                    STATUS          PORTS
8c1f2a9d1b33   billing-api-1              billing:2026.01.03       Up 3 days        0.0.0.0:8080->8080/tcp
2b7d9c0e6c12   tmp_migrate_2025_11        alpine:3.19              Up 90 days       0.0.0.0:9000->9000/tcp

Significado: Buscas “qué sigue vivo” y “qué está ligando puertos”. Nombres como tmp_* son un olor, no una prueba.

Decisión: Para cualquier cosa sospechosa, pasa a la inspección: etiquetas, mounts y la línea de comando.

Tarea 2: Lista de contenedores detenidos que silenciosamente comen disco

cr0x@server:~$ docker ps -a --filter status=exited --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'
CONTAINER ID   NAMES                   IMAGE                 STATUS
a9d0c2f8e1aa   billing-api-1_old       billing:2025.12.10    Exited (137) 2 weeks ago
f1b2a3c4d5e6   debug-shell             ubuntu:22.04          Exited (0) 4 months ago

Significado: Los contenedores exited mantienen su capa escribible y registros hasta que se eliminan.

Decisión: Si están claramente sustituidos y no tienen volúmenes que te importen, elimínalos. Pero primero, verifica los mounts.

Tarea 3: Inspeccionar un contenedor sospechoso por etiquetas (propiedad)

cr0x@server:~$ docker inspect 2b7d9c0e6c12 --format '{{json .Config.Labels}}'
{"com.docker.compose.project":"billing","com.docker.compose.service":"migrate","com.docker.compose.oneoff":"True"}

Significado: Compose lo creó como one-off. Eso a menudo significa docker compose run o un job de migración.

Decisión: Encuentra el proyecto Compose en disco y ve si esto era temporal. Contenedores one-off ejecutándose 90 días rara vez son intencionales.

Tarea 4: Inspeccionar mounts para ver si la eliminación arriesga pérdida de datos

cr0x@server:~$ docker inspect billing-api-1 --format '{{range .Mounts}}{{.Type}} {{.Name}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
volume billing_db_data /var/lib/docker/volumes/billing_db_data/_data -> /var/lib/postgresql/data
bind  - /srv/billing/config -> /app/config

Significado: Este contenedor usa un volumen nombrado para datos de la base. Eliminar el contenedor está bien; eliminar el volumen no está bien a menos que estés absolutamente seguro.

Decisión: Puedes eliminar contenedores antiguos, pero preserva volúmenes hasta probar que no están en uso o tener backups y un plan de restauración.

Tarea 5: Comprobar crecimiento del tamaño del contenedor (capa escribible)

cr0x@server:~$ docker ps -a --size --format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'
NAMES               STATUS                  SIZE
billing-api-1       Up 3 days               12.3MB (virtual 312MB)
tmp_migrate_2025_11 Up 90 days              8.1GB (virtual 18.2GB)

Significado: La capa escribible de tmp_migrate_2025_11 es enorme. Eso suele ser logs, caches o escrituras accidentales en el FS del contenedor.

Decisión: Haz un exec y encuentra qué está creciendo, o captura logs y elimina el contenedor si no debe persistir.

Tarea 6: Identificar qué contenedores están ligando puertos específicos

cr0x@server:~$ docker ps --format '{{.Names}} {{.Ports}}' | grep -E '0\.0\.0\.0:8080|:8080->'
billing-api-1 0.0.0.0:8080->8080/tcp

Significado: El puerto 8080 es propiedad de billing-api-1. Si tu nuevo despliegue no puede iniciarse, por esto es.

Decisión: Verifica si esta es la instancia actual prevista. Si no, deténla limpiamente y despliega correctamente.

Tarea 7: Obtener el comando de arranque completo para entender la intención

cr0x@server:~$ docker inspect tmp_migrate_2025_11 --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'
Entrypoint=["/bin/sh","-lc"] Cmd=["python manage.py migrate && python manage.py collectstatic --noinput && sleep 9999999"]

Significado: Alguien encadenó una migración y luego un sleep infinito. Es un clásico “contenedor temporal que se volvió permanente”.

Decisión: Confirma que las migraciones están hechas y que nadie depende de este contenedor (por ejemplo, volúmenes montados usados en otro lugar). Luego elimínalo.

Tarea 8: Comprobar la política de reinicio (contenedores que vuelven como malas ideas)

cr0x@server:~$ docker inspect billing-api-1 --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
RestartPolicy=unless-stopped

Significado: Si reinicias el host, este contenedor vuelve automáticamente. Eso no es orquestación; es persistencia mediante la política de reinicio.

Decisión: Si quieres que desaparezca, debes eliminarlo (o poner la política de reinicio en no y detenerlo).

Tarea 9: Mapear contenedores a proyectos Compose y detectar “forasteros”

cr0x@server:~$ docker ps -a --format '{{.Names}}' | while read n; do docker inspect "$n" --format '{{.Name}} {{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null; done | sed 's#^/##'
billing-api-1 billing
billing-db-1 billing
tmp_migrate_2025_11 billing
debug-shell <no value>

Significado: debug-shell no tiene la etiqueta de proyecto Compose. Eso lo hace más difícil de atribuir y más probable que sea ejecutado a mano.

Decisión: Para contenedores sin etiqueta, rastrea mediante imagen, comando, mounts y tiempo de creación. No borres a ciegas.

Tarea 10: Encontrar recursos “colgantes” y sin uso con mentalidad de simulacro

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          48        12        21.4GB    15.1GB (70%)
Containers      73        9         9.3GB     8.8GB (94%)
Local Volumes   19        7         120.6GB   22.4GB (18%)
Build Cache     62        0         4.1GB     4.1GB

Significado: Los contenedores son altamente recuperables: probablemente tienes muchos contenedores detenidos o capas escribibles hinchadas.
Los volúmenes son grandes pero menos recuperables; los volúmenes activos están en uso.

Decisión: Empieza con contenedores e imágenes. Toca volúmenes solo después de confirmar que no están en uso y no forman parte de recuperación ante desastres.

Tarea 11: Mostrar qué volúmenes no se usan (pero no los borres aún)

cr0x@server:~$ docker volume ls --format 'table {{.Name}}\t{{.Driver}}'
NAME               DRIVER
billing_db_data    local
billing_cache      local
old_tmp_data       local

cr0x@server:~$ docker volume inspect old_tmp_data --format 'Name={{.Name}} Mountpoint={{.Mountpoint}}'
Name=old_tmp_data Mountpoint=/var/lib/docker/volumes/old_tmp_data/_data

Significado: Listar volúmenes no te dice si están adjuntos. La inspección muestra dónde viven en disco.

Decisión: Cruzar referencias desde contenedores antes de eliminar volúmenes.

Tarea 12: Determinar si algún contenedor aún referencia un volumen

cr0x@server:~$ docker ps -a --format '{{.ID}} {{.Names}}' | while read id name; do docker inspect "$id" --format '{{.Name}} {{range .Mounts}}{{.Name}} {{end}}' | sed 's#^/##'; done | grep -w old_tmp_data || true

Significado: Ninguna salida significa que ningún contenedor monta old_tmp_data.

Decisión: Es candidato para eliminación, después de confirmar que no está destinado a restauraciones futuras o flujos de trabajo manuales.

Tarea 13: Detener un contenedor de forma segura (observar impacto)

cr0x@server:~$ docker stop --time 30 tmp_migrate_2025_11
tmp_migrate_2025_11

Significado: Docker envió SIGTERM y esperó hasta 30 segundos antes de SIGKILL. Detener es reversible: puedes reiniciarlo si hace falta.

Decisión: Observa la salud de la app (peticiones, errores, colas). Si nada cambia, procede a eliminar.

Tarea 14: Eliminar contenedor y confirmar que desapareció

cr0x@server:~$ docker rm tmp_migrate_2025_11
tmp_migrate_2025_11

cr0x@server:~$ docker ps -a --format '{{.Names}}' | grep -w tmp_migrate_2025_11 || echo "not found"
not found

Significado: El contenedor se elimina. Cualquier volumen nombrado permanece a menos que lo borres explícitamente.

Decisión: Vuelve a ejecutar docker system df para verificar el espacio recuperado y asegurarte de que no acabas de borrar algo que estaba en uso.

Tarea 15: Eliminar huérfanos de un proyecto Compose de la forma correcta

cr0x@server:~$ cd /srv/billing
cr0x@server:~$ docker compose ps
NAME            IMAGE                 SERVICE     STATUS
billing-api-1   billing:2026.01.03    api         running
billing-db-1    postgres:15           db          running

cr0x@server:~$ docker compose up -d --remove-orphans
[+] Running 2/2
 ✔ Container billing-db-1   Running
 ✔ Container billing-api-1  Running

Significado: Compose reconcilia el estado del proyecto y elimina contenedores del mismo proyecto que no estén declarados en el archivo Compose actual.

Decisión: Usa esto cuando confíes en el archivo Compose como fuente de la verdad. No lo uses en un directorio que no estés seguro que coincide con producción.

Tarea 16: Poda controlada (la menos mala de las opciones)

cr0x@server:~$ docker image prune -f
Deleted Images:
deleted: sha256:4f9c1a...
Total reclaimed space: 2.3GB

cr0x@server:~$ docker container prune -f
Deleted Containers:
a9d0c2f8e1aa
f1b2a3c4d5e6
Total reclaimed space: 6.7GB

Significado: Image prune elimina imágenes no usadas (no referenciadas por ningún contenedor). Container prune elimina contenedores detenidos.

Decisión: Esto suele ser seguro en setups de host único si entiendes qué significa “no usado”. No es seguro si dependes de contenedores detenidos como artefactos forenses.

Huérfanos de Docker Compose: el sospechoso habitual

La mayoría de las conversaciones sobre “contenedor huérfano” terminan en una discusión sobre Compose, porque Compose es excelente creando contenedores y moderadamente dogmático sobre limpiarlos.
Compose rastrea recursos vía etiquetas y un nombre de proyecto. Ese nombre de proyecto puede venir de:

  • el nombre del directorio desde donde lo ejecutaste,
  • la bandera -p,
  • el campo name: en especificaciones de Compose más nuevas (dependiendo de versiones y herramientas).

Cambia cualquiera de estos y puedes crear un segundo universo de contenedores que parecen no relacionados. No están “huérfanos” desde la perspectiva de Docker.
Están huérfanos desde tu modelo mental.

Compose también crea contenedores “one-off” (etiqueta com.docker.compose.oneoff=True) cuando haces cosas como:

  • docker compose run para migraciones,
  • tareas administrativas ad-hoc,
  • comandos de depuración que terminan con un sleep largo porque alguien quiso “dejarlos ahí”.

El enfoque seguro es tratar Compose como infraestructura declarativa: el archivo es el contrato. Reconcílialo con docker compose up -d --remove-orphans.
Luego deja de hacer one-offs en prod sin un plan de limpieza.

Estrategia de purga segura (lo que realmente hago)

Purgar de forma segura es cuestión de secuencia y evidencia. Quieres eliminar la basura sin borrar aquello que mantiene silenciosamente una integración legacy.
Aquí está la estrategia que escala desde un host único hasta una pequeña flota.

Paso 1: Clasificar contenedores en tres cubos

  • Declarado: creado por una herramienta conocida (proyecto Compose que puedes encontrar, servicio Swarm, etc.).
  • Probablemente declarado: tiene etiquetas o patrones de nombre que sugieren propiedad, pero no encuentras el controlador inmediatamente.
  • No declarado: sin etiquetas, sin repo obvio, nadie lo puede explicar.

Los contenedores declarados son fáciles: arregla el controlador, no el síntoma. Los probablemente declarados requieren arqueología. Los no declarados requieren precaución.

Paso 2: Preferir detener antes que eliminar

Detener te da una palanca de rollback. Si alguien grita, puedes reiniciar. Si nada se rompe tras una ventana razonable de observación, elimina.
“Razonable” depende de la carga: minutos para APIs sin estado detrás de un load balancer; horas o días para jobs tipo cron que se ejecutan semanalmente.

Paso 3: Separar limpieza de contenedores y volúmenes

Los contenedores son desechables. Los volúmenes son donde están los cuerpos enterrados. Una campaña de limpieza segura típicamente:

  • elimina contenedores exited,
  • elimina imágenes y cache de build no usadas,
  • solo entonces evalúa volúmenes, uno por uno, con evidencia.

Paso 4: Poner guardarraíles para futura creación de huérfanos

Si solo limpias una vez, volverás a lo mismo. Guardarraíles que funcionan:

  • Estandarizar nombres de proyecto Compose (-p o nombre explícito) por entorno.
  • Requerir etiquetas para propiedad y referencias a tickets en contenedores ad-hoc.
  • Reportes programados: “contenedores sin etiquetas compose/swarm” es una auditoría barata.
  • Alertas de disco en /var/lib/docker con suficiente margen para actuar antes de outages.

Segundo chiste, porque el universo es injusto: Nada es más permanente que un contenedor temporal iniciado con “Lo elimino después del almuerzo.”

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición equivocada

Una empresa SaaS mediana corría unos cuantos hosts Docker “simples” para servicios internos—exportes de facturación, un par de pipelines ETL y un dashboard.
Sin orquestador. Solo Compose y mucha confianza.

Durante un despliegue rutinario, el ingeniero on-call ejecutó docker system prune -a -f para liberar espacio en disco.
Funcionó. El disco se recuperó. El despliegue prosiguió. Luego los exportes hacia clientes empezaron a fallar con errores de autenticación que no tenían sentido.

La suposición equivocada: “Las imágenes no usadas son seguras de borrar.” En su flujo, un contenedor detenido se conservaba como spare caliente para un job legacy
que solo corría a fin de mes. Ese contenedor referenciaba una imagen que no estaba usada por ningún contenedor en ejecución al momento del prune.

Cuando llegó fin de mes, su automatización intentó iniciar el job al instante. Tirar la imagen requirió acceso a un registry que estaba intermitentemente bloqueado
por cambios en el firewall corporativo. El job no se ejecutó. Finanzas lo notó. Escaló rápido.

La solución no fue “nunca podar.” La solución fue declarar los jobs de fin de mes, hacerlos predecibles y probados, y cachear o espejar las imágenes necesarias.
También aprendieron a tratar el estado “detenido pero importante” como algo real que necesita documentación y monitoreo, no superstición.

Mini-historia 2: La optimización que salió mal

Otra organización quería despliegues más rápidos. Alguien notó que Compose recrea contenedores cuando la configuración cambia, y que los contenedores viejos se acumulan.
Así que “optimizó” cambiando a un script que hacía docker run con nombres de contenedor únicos por build, dejando los antiguos detenidos “por si acaso.”

Al principio se sintió bien. El rollback era “iniciar el contenedor anterior.” Sin pulls al registry, sin espera. El equipo se felicitó.

Luego el host sufrió presión de disco. No por imágenes, sino por contenedores exited y sus capas escribibles. Cada build escribía unos cientos de megabytes de caches
en el filesystem del contenedor. Multiplica por semanas de despliegues y obtienes un fallo en cámara lenta.

Lo que falló fue la complejidad operacional: el mecanismo de rollback era manual y propenso a errores, y la historia de limpieza se volvió “alguien debería podar a veces.”
Cuando el disco se llenó, Docker empezó a fallar al iniciar contenedores, luego el logging se rompió y finalmente incluso las sesiones SSH se comportaron raro porque el filesystem raíz casi estaba lleno.

La solución a largo plazo fue aburrida: usar un patrón de despliegue real con retención explícita (conservar N imágenes previas), almacenar caches en volúmenes o almacenes externos,
y forzar la limpieza como parte del pipeline de despliegue. Reemplazaron el “rollback ad-hoc” por rollbacks repetibles.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una empresa relacionada con pagos corría varios hosts Docker con control de cambios estricto. No era glamoroso, pero era efectivo. Tenían un job semanal programado que recopilaba:
docker ps -a, docker system df, y una lista de contenedores sin etiquetas de propiedad. La salida iba a un ticket interno automáticamente.

Una semana, el informe marcó un nuevo contenedor en ejecución sin etiquetas Compose, ligado a un puerto alto y consumiendo CPU constante.
No era tan pesado como para disparar alertas, pero era extraño.

El ingeniero on-call lo rastreó con docker inspect: montaba un directorio host con secretos de la aplicación, y fue iniciado desde una imagen base genérica.
Eso parecía o un atajo de depuración o algo peor. Lo detuvieron en una ventana controlada y observaron métricas. Nada se rompió.

Tras investigar internamente, resultó ser un contenedor de diagnóstico “temporal” iniciado durante una llamada con un vendor. Se dejó ejecutando y se olvidó.
La práctica aburrida—el informe semanal—lo detectó antes de que se convirtiera en un problema de cumplimiento.

No castigaron al ingeniero que lo inició. Cambiaron el proceso: los contenedores ad-hoc requerían etiquetas y una nota de expiración, y el informe se volvió una verificación diaria en hosts críticos.
Nadie escribió un post en el blog al respecto. Todo siguió funcionando. Ese es el triunfo.

Errores comunes: síntoma → causa raíz → solución

Esta es la sección que lees cuando estás cansado y el host te está haciendo paging. Cada ítem es específico porque el consejo genérico es cómo te llevas sorpresas y downtime.

1) “Eliminé el contenedor pero el uso de disco no cambió”

  • Síntoma: docker rm corre, pero /var/lib/docker sigue enorme.
  • Causa raíz: Volúmenes e imágenes siguen consumiendo espacio; la capa del contenedor no era la culpable principal.
  • Solución: Ejecuta docker system df; poda contenedores detenidos; poda imágenes no usadas; revisa volúmenes individualmente antes de borrarlos.

2) “docker compose down no eliminó el contenedor raro”

  • Síntoma: El proyecto Compose está abajo, pero algunos contenedores permanecen.
  • Causa raíz: El contenedor pertenece a un nombre de proyecto Compose diferente (deriva de directorio o mismatch de -p), o no fue creado por Compose.
  • Solución: Inspecciona etiquetas com.docker.compose.project. Ejecuta Compose desde el directorio correcto con el nombre de proyecto adecuado, o elimina manualmente tras verificar uso.

3) “Un contenedor sigue volviendo después de detenerlo”

  • Síntoma: Detienes un contenedor y se reinicia.
  • Causa raíz: Política de reinicio (always / unless-stopped) o un controlador externo (systemd, Swarm, Kubernetes) lo está recreando.
  • Solución: Identifica el controlador vía etiquetas. Deshabilita el controlador o elimina la definición del servicio. Para política de reinicio, elimina el contenedor o recréalo con --restart=no.

4) “Después de podar, una app no puede arrancar porque falta la imagen”

  • Síntoma: El arranque falla y Docker intenta tirar la imagen inesperadamente.
  • Causa raíz: Pusiste a prune imágenes que no estaban referenciadas por contenedores en ejecución, pero eran necesarias para arranques rápidos o jobs programados.
  • Solución: Declara y prueba los jobs; asegura acceso al registry; mantiene un cache controlado de imágenes; podar con política, no con emoción.

5) “Tras la limpieza, la app arranca pero los datos se perdieron”

  • Síntoma: El servicio corre pero la base/contenido se reseteó.
  • Causa raíz: Eliminaste un volumen nombrado o cambiaste a un volumen anónimo sin darte cuenta.
  • Solución: Deja de borrar volúmenes a la ligera. Verifica mounts con docker inspect. Usa volúmenes nombrados con nombres explícitos. Haz backups de volúmenes y prueba restauraciones.

6) “Hay docenas de contenedores con nombres similares”

  • Síntoma: Contenedores como api_1, api_1_old, api_1_202512, etc.
  • Causa raíz: Scripts de despliegue crean contenedores nuevos en vez de recrear; deriva de proyecto Compose; rollbacks manuales.
  • Solución: Estandariza nombres y controladores. Usa la reconciliación de Compose o un orquestador. Define retención explícita (N imágenes previas), no N contenedores previos.

7) “Los contenedores huérfanos tienen logs enormes”

  • Síntoma: Disco se llena; los logs bajo Docker crecen; los contenedores muestran tamaño grande.
  • Causa raíz: El driver de logging es json-file sin rotación; apps muy parlanchinas; bucles de crash.
  • Solución: Configura rotación de logs en el daemon de Docker o por contenedor; considera logging centralizado; elimina contenedores con logs enormes tras capturar lo necesario.

Listas de verificación / plan paso a paso

Checklist A: “Necesito liberar disco de forma segura hoy”

  1. Obtén una línea base: docker system df y una comprobación del filesystem en /var/lib/docker.
  2. Elimina contenedores detenidos primero: docker container prune (o eliminación manual tras revisión).
  3. Poda cache de build si construyes localmente: docker builder prune (verifica que no dependes de ello para rendimiento durante el incidente).
  4. Poda imágenes no usadas: docker image prune (evita -a salvo que entiendas jobs programados y necesidades de cold-start).
  5. Sólo entonces considera volúmenes: identifica volúmenes no usados; verifica que no están referenciados; verifica posture de backup/restore; borra selectivamente.
  6. Reverifica: docker system df. Confirma que la alerta se limpió y que Docker puede iniciar nuevos contenedores.

Checklist B: “Encontré un contenedor en ejecución desconocido”

  1. No lo borres aún. Identifícalo: docker ps, luego docker inspect para etiquetas y mounts.
  2. Revisa puertos: si enlaza puertos públicos, trátalo como urgente.
  3. Revisa el comando: busca shells de depuración, sleeps o procesos de túnel.
  4. Revisa mounts: paths host y secretos son alto riesgo.
  5. Deténlo con una ventana de observación. Si nada se rompe, elimínalo y abre un ticket para prevenir recurrencia.

Checklist C: “Prevenir que vuelvan los huérfanos”

  1. Estandariza nombres de proyecto Compose (-p fijo por env) y mantiene los archivos Compose en rutas conocidas.
  2. Obliga etiquetas en docker run en producción (propietario, ticket, expiración).
  3. Implementa una auditoría programada: contenedores sin etiquetas de propiedad; volúmenes no referenciados; espacio total recuperable.
  4. Configura rotación de logs y monitorea uso de disco con suficiente antelación.
  5. Convierte trabajos one-off en jobs reales (declarados, repetibles, monitorizados).

FAQ

1) ¿Qué es un “contenedor huérfano” en términos de Docker?

Docker no lo define formalmente. Los operadores usan “huérfano” para decir “un contenedor presente en el host sin un propietario/controlador claro.”

2) ¿Los contenedores huérfanos siempre son seguros de borrar?

No. “Huérfano” a menudo significa “nadie recuerda por qué existe”, que no es lo mismo que “no usado”. Verifica mounts, puertos y tráfico antes de eliminar.

3) ¿Por qué recibo avisos de huérfanos con Docker Compose?

Compose avisa cuando existen contenedores en el mismo proyecto que no están definidos en el archivo Compose actual. Eso ocurre tras renombres, eliminaciones de servicios o deriva de nombre de proyecto.

4) ¿Qué elimina realmente docker compose up -d --remove-orphans?

Contenedores en el mismo proyecto Compose que no están declarados en el archivo Compose actual. No elimina volúmenes a menos que explícitamente le pidas a Compose que los borre.

5) ¿Cuál es el comando “prune” más seguro?

docker container prune suele ser el más seguro porque apunta solo a contenedores detenidos. Luego viene docker image prune para imágenes no usadas.
Ten cuidado con docker system prune -a, especialmente en hosts que corren jobs programados o donde pulls en cold-start son riesgosos.

6) ¿Cómo encuentro quién creó un contenedor?

Empieza con docker inspect y busca etiquetas. Compose, Swarm y muchos sistemas CI ponen labels útiles.
Si faltan etiquetas, inspecciona el comando, mounts, nombre de imagen y tiempo de creación y correlaciónalo con logs de despliegue.

7) Si elimino un contenedor, ¿pierdo datos?

Pierdes la capa escribible del contenedor. Los datos en volúmenes nombrados persisten. Los datos escritos dentro del filesystem del contenedor (no en un volumen) se pierden.
Siempre verifica mounts antes de borrar.

8) ¿Por qué los contenedores exited ocupan tanto espacio?

Los contenedores exited mantienen su capa de filesystem y registros. Una app muy habladora usando el logging por defecto JSON puede generar archivos grandes incluso cuando el contenedor ya no se ejecuta.

9) ¿Por qué reaparecen contenedores después del reboot?

Políticas de reinicio como unless-stopped pueden devolverlos, y controladores externos pueden recrearlos. Identifica qué mecanismo aplica antes de asumir “Docker está embrujado.”

10) ¿Cómo evito contenedores huérfanos durante mantenimiento puntual?

Prefiere ejecutar one-offs como jobs declarados (servicio Compose, tarea programada o job del orquestador). Si debes usar docker run, añade etiquetas y un plan de limpieza,
y evita sleeps largos.

Próximos pasos que puedes hacer hoy

Si solo haces tres cosas, haz estas:

  1. Ejecuta docker system df e identifica si son contenedores, imágenes, volúmenes o cache de build los que realmente hogarean disco.
  2. Audita la propiedad: lista contenedores sin etiquetas Compose/stack e inspecciona sus mounts y puertos antes de tocarlos.
  3. Pon en marcha una rutina recurrente de limpieza y reporte, y luego estandariza nombres de proyecto Compose para dejar de crear universos paralelos.

Los contenedores huérfanos no son un misterio de Docker. Son un fallo de gestión del ciclo de vida. Arregla el ciclo de vida, y los “huérfanos” desaparecerán en gran parte—junto con las alertas de disco a las 2 a.m.

← Anterior
refquota de ZFS: La única cuota que detiene las “mentiras” del espacio usado
Siguiente →
Debian 13 «Solicitud de inicio repetida demasiado rápido»: correcciones de systemd que realmente perduran

Deja un comentario