Docker “Text file busy” durante el despliegue: la solución que evita reinicios intermitentes

¿Te fue útil?

Haces un despliegue. El contenedor se reinicia. Y luego se vuelve a reiniciar. Los logs muestran un clásico: Text file busy.
A veces funciona en el segundo intento, a veces tras cinco, y a veces solo cuando lo estás mirando.

Este error es un mensaje pequeño con consecuencias largas: Linux te dice que intentaste modificar o ejecutar algo
que el kernel todavía considera “en uso”. En producción aparece como reinicios intermitentes, fallos misteriosos en despliegues
y equipos culpando a Docker cuando el verdadero culpable es cómo distribuyes los archivos.

Qué significa realmente “Text file busy” (y por qué Docker se lleva la culpa)

En Linux, Text file busy normalmente corresponde a ETXTBUSY: intentaste una operación sobre un
archivo ejecutable (históricamente llamado “text” porque el segmento de texto contiene instrucciones) mientras está siendo
ejecutado o está de alguna forma anclado por el kernel que bloquea tu acción.

En el mundo de los contenedores esto ocurre durante los despliegues porque mezclamos dos mundos:

  • Imágenes casi inmutables (genial): las capas están direccionadas por contenido, se construyen una vez y se ejecutan muchas.
  • Montajes bind mutables (peligroso): archivos del host aparecen en vivo dentro del contenedor.

El patrón clásico de fallo: un job de CI, un script de despliegue o un sidecar actualiza un binario o script de entrypoint en una ruta montada
mientras un contenedor existente todavía está arrancando, cerrándose o reiniciándose. El kernel rechaza la operación en el peor momento.
Docker solo entrega el mensaje. Docker también es el mensajero al que es cómodo gritarle, porque no puede devolver el saludo.

La solución que evita la inestabilidad es aburrida y absoluta: nunca actualices ejecutables in situ en una ruta que un contenedor en ejecución pueda ejecutar.
Construye un archivo nuevo y luego cambia de forma atómica lo que señala “actual” (típicamente mediante un intercambio de symlink o swap de bind mount),
o deja de usar bind mounts para ejecutables por completo.

La cita que deberías pegar cerca de tus scripts de despliegue

“La esperanza no es una estrategia.” (idea parafraseada común en círculos de ops/confiabilidad)

Si tu despliegue depende de “ojalá el proceso viejo salga antes de que sobrescribamos el archivo”, no estás desplegando. Estás apostando con mejor marca.

Broma #1: Los contenedores son ganado, pero el que contiene tu script de despliegue siempre es una mascota. Tiene nombre, y muerde.

Guía de diagnóstico rápido

Esta es la secuencia de “tengo 10 minutos antes de que el release manager inicie un hilo”. El objetivo no es la pureza filosófica.
El objetivo es identificar si tratas con (a) mutación de bind mount, (b) comportamiento de overlayfs/capas, (c) carrera de apagado, o (d) otra cosa.

Primero: encuentra el archivo exacto que está “ocupado”

  • Desde los logs, extrae la ruta: /app/bin/service, /entrypoint.sh, /usr/local/bin/foo.
  • Si los logs no lo muestran, ejecuta el comando fallido con strace (ver tareas abajo) para capturar qué archivo devuelve ETXTBUSY.

Segundo: determina si esa ruta es un bind mount

  • docker inspect.Mounts para el contenedor.
  • Si es un bind mount, probablemente has encontrado al culpable.

Tercero: identifica qué proceso todavía lo tiene abierto/ejecutando

  • Usa lsof o fuser en el host para la ruta del host.
  • Si está dentro del sistema de ficheros del contenedor (overlay2), revisa procesos en ejecución y sus rutas ejecutables.

Cuarto: decide en qué modo de fallo estás

  • El script de despliegue sobrescribió un ejecutable en ejecución → arregla el método de despliegue (cambio atómico), deja de editar in situ.
  • Entrypoint montado por bind que se reemplaza → deja de montar scripts de entrypoint; insértalos en la imagen o móntalos como solo lectura con cambio de versión.
  • El apagado tarda demasiado → maneja SIGTERM, añade preStop/wait, aumenta el grace, no mates forzosamente pronto.
  • Proceso que se auto-actualiza (sí, todavía existe esto) → elimina la autoactualización; entrega una nueva imagen en su lugar.

Quinto: aplica una mitigación fiable mientras trabajas en la solución real

  • Deja de sobrescribir; escribe en una ruta nueva y renombra/intercambia symlink.
  • Monta ejecutables como solo lectura.
  • Deshabilita temporalmente “restart always” para evitar una tormenta de reinicios que oculte la causa raíz.

Causas raíz: las 8 formas de ganarte ETXTBUSY

1) Sobrescritura in situ de un binario en ejecución (bind mount o volumen compartido)

Alguien hace cp new-binary /srv/app/bin/service mientras el servicio antiguo sigue en ejecución, o un contenedor está en medio de arrancar.
Linux permite muchas operaciones sobre archivos abiertos, pero reemplazar un binario en ejecución puede disparar ETXTBUSY según la secuencia y el sistema de archivos.
El mensaje es tu única advertencia de que tu modelo de despliegue vive peligrosamente.

2) Reemplazo de un script de entrypoint que se está ejecutando

Los scripts de shell también son ejecutables. Si tu contenedor arranca con /entrypoint.sh desde un bind mount, y tu despliegue actualiza ese archivo,
puedes recibir “text file busy” durante el arranque—justo cuando tu orquestador está haciendo muchos reinicios, checks de salud e impaciencia.

3) CI/CD escribe en un directorio que también es el directorio de runtime

El enfoque “simple”: un job construye artefactos y los deja en /srv/app/current. Otro job reinicia el contenedor.
Si esos pasos se solapan o se reejecutan, has creado una condición de carrera con producción como árbitro.

4) Dos contenedores comparten la misma ruta del host y despliegan desincronizados

Un contenedor sigue ejecutando código antiguo desde un bind mount compartido; otro contenedor actualiza ese mount.
Felicidades: has implementado contención de bloqueo distribuido usando solo scripts de shell.

5) Políticas de reinicio agresivas que crean DoS autoinfligido

restart: always está bien cuando la falla es rara. Cuando la falla la desencadena un paso de despliegue que sigue ocurriendo,
obtienes un bucle de reinicios cerrado. Ese bucle incrementa las probabilidades de chocar con la ventana de reemplazo de archivos.
El error se vuelve “flaky” porque el timing cambia.

6) Rarezas de overlayfs cuando mutas rutas que debían ser inmutables

El driver overlay2 de Docker está diseñado para capas copy-on-write. La mayoría de las veces se comporta como un sistema de archivos normal.
Pero cuando intentas hacer cosas ingeniosas—como intercambiar ejecutables en caliente en capas escribibles durante el arranque—estás dependiendo de sutilezas:
whiteouts, copy-up y semánticas por capa. Puede que no veas ETXTBUSY siempre, pero lo verás cuando menos lo quieras.

7) Malentendidos sobre atomicidad: rename es atómico, copy no lo es

La gente dice “lo actualizamos de forma atómica” y luego te muestra un cp. Copiar un archivo sobre otro no es atómico de la forma que necesitas.
Un rename dentro del mismo sistema de archivos es atómico; un copy seguido de overwrite es una invitación a lecturas parciales y errores extraños.

8) Apagados largos + kill forzado + reinicio inmediato

Si tu servicio tarda en salir y tu orquestador lo mata temprano, el proceso puede seguir presente durante el siguiente intento de arranque
(o el sistema de archivos todavía tiene referencias de ejecución). Los bucles cerrados amplifican esto.
A menudo la solución no es “sleep 5” (aunque “funciona”), sino hacer el apagado determinista y que los pasos de despliegue no se solapen.

Broma #2: Añadir sleep 10 para arreglar una condición de carrera es como arreglar un tejado con goteras comprando lluvia más ruidosa.

La solución duradera: deja de editar ejecutables in situ

Si recuerdas una cosa: despliega cambiando punteros, no mutando archivos en vivo.
Eso significa directorios de release, symlinks y mounts solo lectura, o construir una nueva imagen y cambiar contenedores.
Quieres que el runtime vea un artefacto consistente y completo, cada vez.

Cómo se ve “bien”

  • Los artefactos son inmutables: un binario o script se escribe una vez y luego no se modifica.
  • La activación es atómica: cambias de la versión A a la B usando una operación atómica (renombrar un symlink, actualizar el objetivo de un bind mount).
  • Rollback es la misma operación: volver a la versión previa con el mismo cambio de puntero.
  • Los contenedores no comparten ejecutables mutables: si comparten algo, es datos, no código.

Los dos patrones de despliegue que realmente funcionan

Patrón A: Incluir ejecutables en la imagen (recomendado)

La imagen del contenedor es el artefacto. Desplegar significa: tirar la nueva imagen, arrancar contenedor nuevo, parar el contenedor viejo.
No bind mounts para /app/bin. No “parcheeo en vivo” dentro del contenedor. Para eso se creó Docker.

Patrón B: Directorios de release + intercambio atómico de symlink (cuando debes usar bind mounts)

A veces estás atado: restricciones regulatorias, artefactos gigantes, builds air-gapped, supuestos legados en tiempo de ejecución.
Bien. Entonces hazlo como adultos:

  • Escribe la nueva release en /srv/app/releases/2026-01-03_120501/.
  • Verifícala (checksums, permisos, smoke test).
  • Actualiza de forma atómica el symlink /srv/app/current para que apunte a la nueva release.
  • Reinicia contenedores que monten /srv/app/current como solo lectura.

La clave es que /srv/app/current cambia instantáneamente como puntero; el contenido del directorio de release nunca cambia.
Eso elimina el “ejecutable medio-copiado” y reduce mucho el “text file busy” porque no estás sobrescribiendo el archivo que se está ejecutando.
Si algo sigue ejecutando el binario antiguo, continúa ejecutándolo desde su inode antiguo. Los contenedores nuevos arrancan sobre el inode nuevo.
Así compras cordura con las semánticas del sistema de archivos.

Pequeñas pero importantes decisiones de endurecimiento

  • Monta el código como solo lectura dentro de los contenedores. Si algo intenta mutarlo, fallará de forma ruidosa.
  • Nunca montes por encima /usr/local/bin a menos que te guste la arqueología.
  • Haz los entrypoints inmutables (inclúyelos en la imagen). Si debes montarlos, monta una ruta versionada y cambia vía symlink.
  • Controla los reinicios: evita bucles infinitos que enmascaren fallos reales; usa backoff y alertas.

Tareas prácticas: 12+ comandos que te dicen qué está pasando

Estos son los comandos que ejecutas cuando el despliegue falla y la gente sugiere “simplemente reinicia el nodo”.
Cada tarea incluye: comando, salida de ejemplo, qué significa y la decisión que tomas.

Task 1: Confirma la firma del fallo en los logs del contenedor

cr0x@server:~$ docker logs --tail=80 api-1
exec /app/bin/api: text file busy

Qué significa: El kernel rechazó un execve() de /app/bin/api (o un shell lo invocó) con ETXTBUSY.

Decisión: Identificar si /app/bin/api proviene de una capa de imagen o de un mount. Si es un mount, deja de actualizarlo in situ.

Task 2: Inspecciona mounts y encuentra bind mounts rápidamente

cr0x@server:~$ docker inspect api-1 --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/app/current","Destination":"/app","Mode":"ro","RW":false,"Propagation":"rprivate"}]

Qué significa: /app es un bind mount desde la ruta host /srv/app/current. Si scripts de despliegue modifican archivos bajo ese árbol, puedes correr riesgo de carrera con la ejecución.

Decisión: Verifica si /srv/app/current es un symlink a releases versionadas. Si no lo es, impléméntalo.

Task 3: Comprueba si “current” es un symlink (y a dónde apunta)

cr0x@server:~$ ls -l /srv/app/current
lrwxrwxrwx 1 deploy deploy 44 Jan  3 11:58 /srv/app/current -> /srv/app/releases/2026-01-03_115801

Qué significa: Buena señal: current es un puntero. Si el despliegue actualiza el symlink de forma atómica, los contenedores verán cortes limpios.

Decisión: Asegúrate de que el despliegue escriba en un nuevo directorio de release y nunca edite el contenido de la release apuntada tras la activación.

Task 4: Detecta comportamiento de “copiar in situ” en scripts de despliegue

cr0x@server:~$ grep -R --line-number -E 'cp .* /srv/app/current|rsync .* /srv/app/current' /srv/deploy/scripts
/srv/deploy/scripts/deploy.sh:83:cp build/api /srv/app/current/bin/api

Qué significa: Alguien está copiando directamente en el árbol en vivo. Esa es la carrera.

Decisión: Cambia el despliegue para preparar en un nuevo directorio y hacer swap de symlink, o construye una nueva imagen y redeploy.

Task 5: Identifica quién mantiene el archivo abierto (lado host)

cr0x@server:~$ sudo lsof /srv/app/releases/2026-01-03_115801/bin/api | head
COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF    NODE NAME
api     23144  1001 txt    REG  253,0  834912 4123912 /srv/app/releases/2026-01-03_115801/bin/api

Qué significa: PID 23144 está ejecutando el binario (el mapeo txt). Sobrescribir ese inode es exactamente cómo disparas ETXTBUSY y peores cosas.

Decisión: No sobrescribas ese archivo. Despliega un inode nuevo (ruta nueva) y cambia por symlink; o detén el proceso limpiamente antes de cualquier reemplazo.

Task 6: Usa fuser para confirmar procesos que usan el ejecutable

cr0x@server:~$ sudo fuser -v /srv/app/releases/2026-01-03_115801/bin/api
                     USER        PID ACCESS COMMAND
/srv/app/releases/2026-01-03_115801/bin/api:
                     1001     23144 ...e.  api

Qué significa: Mismo resultado, herramienta diferente: un proceso está ejecutando el archivo.

Decisión: Corrige el despliegue para evitar reemplazo de archivos; no “reintentes hasta que funcione”.

Task 7: Ve la vista del contenedor sobre el ejecutable y confirma que coincide con el mount

cr0x@server:~$ docker exec api-1 readlink -f /app/bin/api
/srv/app/current/bin/api

Qué significa: El contenedor está ejecutando desde el árbol montado por bind.

Decisión: Trata /srv/app como almacenamiento de código en producción. Versiona en disco; monta solo lectura; cambia punteros de forma atómica.

Task 8: Prueba si el archivo se está modificando durante el despliegue (inotify)

cr0x@server:~$ sudo inotifywait -m /srv/app/current/bin -e create,modify,move,delete
Setting up watches.
Watches established.
/srv/app/current/bin/ MODIFY api

Qué significa: Algo está modificando api in situ. Ese es tu arma humeante.

Decisión: Elimina la mutación in situ. Si necesitas actualizar, escribe api.new, verifica y luego renombra o intercambia symlink.

Task 9: Comprueba el comportamiento de bucle de reinicios de Docker

cr0x@server:~$ docker ps --filter name=api-1 --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES   STATUS                          IMAGE
api-1   Restarting (1) 3 seconds ago    api:prod

Qué significa: El contenedor está fluctuando. Eso amplifica las carreras de tiempo y hace que los logs sean más difíciles de interpretar.

Decisión: Detén temporalmente el contenedor para estabilizar el sistema, luego arregla el método de despliegue; o añade backoff en el orquestador.

Task 10: Inspecciona el estado del contenedor para el último error

cr0x@server:~$ docker inspect api-1 --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}'
restarting 1 

Qué significa: Docker no capturó una cadena de “error” separada aquí; necesitas logs + trazado de llamadas al sistema para detalle.

Decisión: Usa strace en la ruta exec fallida o reproduce en un contenedor one-shot para capturar el origen de ETXTBUSY.

Task 11: Reproduce con una ejecución one-shot que imprima el exec fallido

cr0x@server:~$ docker run --rm -v /srv/app/current:/app:ro api:prod /app/bin/api --version
bash: /app/bin/api: Text file busy

Qué significa: Incluso una ejecución one-shot limpia choca con el problema. Eso sugiere que la ruta del host está siendo modificada activamente o el archivo está en un estado intermedio extraño.

Decisión: Detén el pipeline de despliegue, verifica la integridad del archivo (tamaño, checksum, permisos) y confirma que nadie escriba en la ruta en vivo.

Task 12: Confirma si el archivo se está reemplazando vía rename (bueno) o sobrescribiendo (malo)

cr0x@server:~$ sudo auditctl -w /srv/app/current/bin/api -p wa -k api-bin-watch
cr0x@server:~$ sudo ausearch -k api-bin-watch | tail -n 6
type=SYSCALL msg=audit(1735906101.220:911): arch=c000003e syscall=2 success=yes exit=3 a0=7f5b7a3c a1=241 a2=1b6 a3=0 items=1 ppid=1102 pid=28440 auid=1000 uid=1000 gid=1000 exe="/usr/bin/cp" key="api-bin-watch"

Qué significa: Capturaste cp escribiendo directamente en el ejecutable. Eso no es atómico y colisiona con la ejecución.

Decisión: Reemplaza “copiar por encima” por “escribir nuevo archivo y renombrar” o “preparar release y swap de symlink”.

Task 13: Verifica el comportamiento de rename atómico en tu directorio de despliegue

cr0x@server:~$ cd /srv/app
cr0x@server:~$ ln -sfn /srv/app/releases/2026-01-03_115801 current.new
cr0x@server:~$ mv -Tf current.new current

Qué significa: mv -T trata el destino como un archivo; -f fuerza el reemplazo. Este es un patrón común y fiable para actualizaciones atómicas de symlink en Linux.

Decisión: Estandariza esto como el paso de activación. No copies archivos parcialmente dentro de current.

Task 14: Confirma que el mount es solo lectura dentro del contenedor

cr0x@server:~$ docker exec api-1 sh -lc 'mount | grep " /app "'
/dev/sda1 on /app type ext4 (ro,relatime,errors=remount-ro)

Qué significa: El contenedor no puede modificar código bajo /app. Eso es bueno: previene comportamientos de auto-modificación y mantiene la culpa en la pipeline de despliegue donde pertenece.

Decisión: Manténlo como solo lectura. Si algo falla porque esperaba escribir ahí, arregla la app para que escriba en un volumen de datos.

Task 15: Valida el tiempo de apagado para evitar ejecuciones solapadas

cr0x@server:~$ docker stop -t 30 api-1
api-1
cr0x@server:~$ docker ps -a --filter name=api-1 --format 'table {{.Names}}\t{{.Status}}'
NAMES   STATUS
api-1   Exited (0) 3 seconds ago

Qué significa: El proceso sale limpiamente dentro del periodo de gracia. Si no fuera así, verías comportamiento de kill forzado y mayor probabilidad de choques entre reinicios y pasos de despliegue.

Decisión: Si el apagado es lento, arregla el manejo de señales, añade hooks preStop y ajusta timeouts. No compenses con “reintentos de despliegue”.

Task 16: Usa strace para confirmar la syscall que devuelve ETXTBUSY

cr0x@server:~$ strace -f -e trace=execve,openat,rename,unlink -s 256 docker run --rm -v /srv/app/current:/app:ro api:prod /app/bin/api --version
execve("/usr/bin/docker", ["docker", "run", "--rm", "-v", "/srv/app/current:/app:ro", "api:prod", "/app/bin/api", "--version"], 0x7ffd1efc8b10 /* 36 vars */) = 0
...
execve("/app/bin/api", ["/app/bin/api", "--version"], 0x55d2b5d3d3a0 /* 14 vars */) = -1 ETXTBUSY (Text file busy)

Qué significa: Sin adivinanzas. El kernel devolvió ETXTBUSY en execve de esa ruta.

Decisión: Trátalo como un bug de ciclo de vida del artefacto. Cambia cómo se produce y activa el archivo; no “tunes Docker”.

Tres microhistorias del mundo corporativo

Microhistoria #1: El incidente causado por una suposición equivocada

Una fintech mediana ejecutaba una pila Docker Compose en unas cuantas VMs potentes. Distribuían un binario Go y un par de scripts vía bind mount:
/srv/finapp/current montado en /app. La suposición era simple y equivocada: “Linux permite reemplazar archivos mientras están en uso”.

El job de despliegue hacía cp del nuevo /srv/finapp/current/bin/api y ejecutaba inmediatamente docker compose up -d.
En días tranquilos funcionaba. En días ocupados, algunos contenedores se reiniciaban, encontraban Text file busy y fluctuaban. Sonó el pager.
La gente culpó a “Docker por inestable”, porque el error aparecía al arrancar el contenedor, no durante el copy.

La revisión post-incident mostró el verdadero modo de fallo: varias instancias de la app ejecutaban bin/api desde el bind mount,
mientras el despliegue lo reemplazaba in situ. A veces el copy y el exec colisionaban. A veces el copy escribía parcialmente y el siguiente exec
obtenía un error distinto. Habían construido una carrera dependiente del timing, la planificación de CPU y una buena dosis de mala suerte.

La solución no fue exótica. Prepararon artefactos en /srv/finapp/releases/<id>, los verificaron y cambiaron atómicamente
current usando mv -Tf. Además montaron /app solo lectura para garantizar que ningún contenedor pudiera mutarlo.
El siguiente despliegue fue aburrido, que es el tono emocional correcto para un despliegue.

Microhistoria #2: La optimización que salió mal

Una empresa de ad-tech quería despliegues más rápidos. Estaban cansados de construir imágenes, así que probaron “inyección de artefactos”:
construir en el host y montarlo en el contenedor. También activaron una política de reinicio agresiva para que los servicios “se recuperaran”.

Era rápido. También era una gran manera de convertir una carrera de despliegue inocua en un festival de reinicios a nivel de clúster.
Cuando un despliegue arrancaba, los contenedores se reiniciaban rápido, algunos tomaban el archivo mientras estaba a medio actualizar, y varios golpearon ETXTBUSY.
La política de reinicio actuó, reiniciándolos otra vez, lo que aumentó la probabilidad de colisión con la ventana de despliegue. Bucle de realimentación logrado.

El equipo “arregló” añadiendo llamadas sleep entre copy y restart, luego más sleep cuando el primero no era suficiente.
Los despliegues se hicieron más lentos. Los fallos fueron menos frecuentes. Luego un día particularmente ocupado cambió el timing de nuevo.
El error volvió como una alergia estacional.

La solución real fue dejar de optimizar lo equivocado. Volvieron a construir imágenes para producción y solo usaron bind mounts en dev.
Para los pocos servicios que aún necesitaban assets montados en host, usaron directorios de release y swap de symlink. El tiempo de despliegue subió un poco.
El tiempo de incidentes bajó mucho. Ese es el intercambio que quieres.

Microhistoria #3: La práctica aburrida pero correcta que salvó el día

Un proveedor SaaS del sector salud tenía una política estricta de “artefactos inmutables”. Los ingenieros se quejaban de ello como se queja la gente de los cinturones de seguridad.
Cada despliegue producía un directorio de release versionado con checksums y un manifiesto. La activación era un swap de symlink. El rollback la misma operación.

Un viernes, un fallo de almacenamiento hizo que un job de despliegue reintentara mientras estaba en staging. Un segundo job empezó antes de que el primero terminara.
Eso habría sido un incidente clásico de “text file busy”, porque ambos jobs atacaban la misma app. Pero no atacaron el mismo directorio.
Cada ejecución de staging escribió en una ruta de release única y nueva.

El paso de activación usó un lock y una actualización atómica de current. Solo un job ganó. El otro falló rápido y de forma ruidosa.
El servicio nunca vio artefactos parciales. Los contenedores se reiniciaron exactamente una vez. Nadie aprendió un nuevo mensaje de error ese día.

El postmortem fue corto. La mejora fue hacer el lock más explícito y mejorar las alertas sobre contención de despliegues.
La práctica que los salvó no fue heroica. Fue buena higiene de sistemas de archivos aplicada de forma consistente, que es cómo se logra la mayor parte de la confiabilidad.

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

1) Síntoma: “Text file busy” solo durante el despliegue, luego desaparece

Causa raíz: Actualización in situ del artefacto que colisiona con reinicios de contenedores; carrera dependiente del timing.

Solución: Preparar en directorio versionado; cambio atómico de symlink; montar código en tiempo de ejecución como solo lectura.

2) Síntoma: Sucede más cuando el tráfico es alto

Causa raíz: Apagado lento o arranque más largo que aumentan la ventana de solapamiento; bucles de reinicio amplifican las colisiones.

Solución: Haz el apagado determinista (manejo de SIGTERM), aumenta el periodo de gracia, añade backoff; evita tormentas de reinicio inmediatas durante despliegues.

3) Síntoma: Solo un nodo muestra el problema

Causa raíz: Comportamiento específico del script de despliegue en ese nodo, diferente sistema de archivos (NFS vs ext4 local), o distintas opciones de montaje.

Solución: Compara tipos y opciones de mount, estandariza el mecanismo de despliegue, evita ejecutar desde shares de red cuando sea posible.

4) Síntoma: “Text file busy” para entrypoint.sh o scripts de arranque

Causa raíz: El entrypoint está montado por bind y se actualiza; o la gestión de configuración lo reescribe.

Solución: Incluye el entrypoint en la imagen; o versiona los scripts y cambia punteros, nunca sobrescribas.

5) Síntoma: El despliegue usa rsync y aún falla

Causa raíz: rsync actualiza archivos in situ por defecto; los archivos temporales/rename dependen de flags.

Solución: Usa un directorio de staging; o rsync hacia una ruta de release nueva; luego swap de symlink. No rsync dentro de “current”.

6) Síntoma: “Pero usamos rename, debería ser atómico”

Causa raíz: Rename es atómico solo dentro del mismo sistema de archivos y solo para la operación rename; tu proceso podría todavía estar copiando o sobrescribiendo.

Solución: Asegura que staging y activación ocurran en el mismo filesystem; usa mv -Tf en symlinks; evita movimientos entre sistemas de archivos distintos.

7) Síntoma: El contenedor falla con “permission denied” después de que lo “arreglaste”

Causa raíz: El directorio de release nuevo tiene propietario/permisos incorrectos; mount solo lectura expone empaquetado descuidado.

Solución: Establece permisos correctos en el paso de build; verifica con stat; añade un smoke test previo a la activación.

8) Síntoma: Solo ocurre en Kubernetes, no localmente

Causa raíz: Hooks de ciclo de vida, reprogramación rápida y readiness/liveness crean un timing más agresivo. Además, volúmenes compartidos son más comunes.

Solución: Evita volúmenes ejecutables compartidos; usa imágenes; si debes usar volúmenes, usa rutas versionadas y actualizaciones atómicas de punteros además de proper terminationGracePeriodSeconds.

Listas de verificación / plan paso a paso

Checklist: Contención inmediata (hoy)

  1. Detén la tormenta de reinicios: deshabilita temporalmente reinicios automáticos o reduce la escala del servicio que falla.
  2. Congela jobs de despliegue que escriben en la ruta de runtime.
  3. Identifica la ruta ocupada a partir de logs o strace.
  4. Verifica si esa ruta está bind-mounted; si es así, trátala como sospechosa principal.
  5. Usa lsof/fuser en el host para confirmar qué proceso la está ejecutando.
  6. Si es necesario, detén el servicio limpiamente antes de más cambios. Evita bucles de kill forzado.

Checklist: Remediación duradera (esta semana)

  1. Decide tu modelo de artefacto:
    • Preferible: incluir binarios en la imagen y redeplegar contenedores.
    • Fallback: directorios de release versionados + swap de symlink.
  2. Cambia el despliegue para preparar artefactos en un directorio único:
    • Escribe: /srv/app/releases/<release-id>/
    • Nunca escribas en: /srv/app/current/
  3. Añade verificación antes de la activación:
    • Validación checksum/tamaño
    • Comprobación de permisos (+x)
    • Prueba básica de ejecución (--version o --help)
  4. Activa mediante cambio atómico de puntero:
    • ln -sfn ... current.new
    • mv -Tf current.new current
  5. Monta la ruta de código como solo lectura en Docker/Compose/Kubernetes.
  6. Asegura que el apagado sea graceful:
    • Manejar SIGTERM
    • Configurar timeouts de parada razonables

Checklist: Guardarraíles (este trimestre)

  1. Añade una comprobación en CI que falle builds si los scripts de despliegue copian dentro de “current”.
  2. Añade auditoría/inotify durante canary deploys para asegurar que no ocurre modificación in situ.
  3. Estandariza rutas de artefactos y patrones de mount entre servicios.
  4. Registra contención de despliegues: si dos despliegues se solapan, falla uno inmediatamente con un error claro.
  5. Haz que los rollbacks sean de primera clase: conserva releases previas en disco; cambia el symlink de vuelta.

Datos interesantes y contexto histórico

  • ETXTBUSY es equipaje antiguo de Unix con consecuencias modernas: el nombre viene de “text segment”, el mapeo de código ejecutable en Unix temprano.
  • Linux puede unlinkear ejecutables en ejecución: un proceso puede seguir corriendo incluso si su ejecutable se elimina, porque mantiene la referencia al inode abierto.
  • Rename atómico es una de las garantías más fuertes del sistema de archivos: en sistemas POSIX, renombrar dentro del mismo filesystem es atómico, por eso los swaps de symlink funcionan.
  • Los filesystems copy-on-write cambiaron las expectativas: overlayfs y las imágenes por capas fomentan inmutabilidad, pero los bind mounts reintroducen mutabilidad justo donde duele.
  • Los scripts de shell también pueden disparar ETXTBUSY: cualquier cosa ejecutada vía execve puede estar “ocupada”, no solo binarios compilados.
  • Los bucles de reinicio ocultan las causas raíz: los orquestadores reintentan rápido; los logs rotan; el primer mensaje de fallo desaparece bajo una pila de reinicios idénticos.
  • NFS y filesystems de red añaden su propio sabor: la consistencia close-to-open y el caching pueden producir comportamientos dependientes del timing que parecen ETXTBUSY o actualizaciones parciales.
  • “Hot patching” suele ser olor a mal despliegue: los servicios que se autoactualizan eran más comunes antes de los contenedores; con imágenes suele ser casi siempre la decisión equivocada.
  • Los releases basados en symlink son anteriores a los contenedores: el patrón tiene décadas en web hosting y app servers porque encaja con cómo se comportan kernels y sistemas de archivos.

Preguntas frecuentes

1) ¿Es “Text file busy” un bug de Docker?

Casi nunca. Es el kernel rechazando una operación (normalmente execve o una actualización de archivo) debido a cómo se está usando el archivo.
Docker solo es el lugar donde lo ves.

2) ¿Por qué solo ocurre a veces?

Las carreras dependen de la planificación. Carga de CPU, latencia de I/O, timing de reinicios y si otro job de despliegue se solapa cambian la ventana.
“A veces” es exactamente cómo se presentan las condiciones de carrera.

3) ¿Puedo arreglarlo añadiendo reintentos o sleeps?

Puedes enmascararlo. No lo vas a arreglar. Estás apostando a que el timing sea más amable la próxima vez, lo cual no es un contrato que puedas garantizar.
La solución real es dejar de sobrescribir ejecutables in situ y activar releases de forma atómica.

4) ¿Montar el directorio como solo lectura ayuda?

Sí, como guardarraíl. Evita que los contenedores modifiquen su propio código y hace visibles las suposiciones incorrectas rápido.
No arregla un script de despliegue en el host que aún sobrescribe los archivos, así que combínalo con staging de releases correcto.

5) ¿Y si debo usar bind mounts para código (restricciones legadas)?

Usa directorios de release versionados y un puntero symlink como /srv/app/current. Nunca copies dentro de current.
Monta current como solo lectura en los contenedores. Cambia el symlink de forma atómica.

6) ¿Por qué ayuda cambiar un symlink si los procesos siguen ejecutando código viejo?

Porque los procesos ejecutan inodos, no cadenas de ruta. Un proceso en ejecución sigue usando su inode ya abierto.
Los procesos nuevos resuelven el symlink hacia un inode nuevo. Evitas editar el inode del que depende un proceso en ejecución.

7) ¿Pasa esto con overlay2 incluso sin bind mounts?

Puede, pero es menos común. La mayoría de los problemas ETXTBUSY en despliegues vienen de mutar archivos montados desde el host.
Si modificas archivos dentro de un contenedor en tiempo de ejecución (especialmente ejecutables), recreas el mismo problema dentro de overlayfs.

8) ¿Cómo pruebo qué proceso es responsable?

Usa lsof o fuser en la ruta del host y confirma el mapeo de mount del contenedor con docker inspect.
Si hace falta, usa strace para atrapar el execve que devuelve ETXTBUSY.

9) ¿Es rsync seguro si uso –inplace o –delay-updates?

--inplace es activamente inseguro para ejecutables en vivo. --delay-updates es mejor, pero el patrón más seguro sigue siendo:
rsync hacia un directorio de release nuevo, verifica y luego cambia el puntero.

10) ¿Cuál es la solución estructural más rápida con menor drama organizacional?

Conserva tu estructura de bind mounts existente, pero cambia el despliegue para crear un nuevo directorio de release y hacer swap atómico de symlink.
Es de bajo impacto, alto valor y fácil de auditar.

Conclusión: qué cambiar el lunes por la mañana

“Text file busy” durante despliegues Docker no es un misterio cósmico. Es tu sistema de archivos diciéndote que tu método de despliegue es inseguro.
La solución tampoco es misteriosa: deja de mutar ejecutables in situ y activa releases de forma atómica.

Pasos siguientes que dan retorno inmediato:

  1. Encuentra la ruta que dispara ETXTBUSY y confirma si está bind-mounted.
  2. Elimina copias in situ en el directorio runtime en vivo.
  3. Adopta despliegues basados en imagen o directorios de release + swap de symlink.
  4. Monta el código como solo lectura y haz los entrypoints inmutables.
  5. Reduce la tormenta de reinicios para que los fallos sean visibles, no difuminados por reintentos.

El objetivo no es “nunca ver ETXTBUSY otra vez.” El objetivo es construir despliegues que no dependan de la suerte, del timing o de la fase actual de la luna.
Tu yo futuro seguirá de guardia. Hazle un favor.

← Anterior
Eficiencia: por qué más rápido no siempre es mejor
Siguiente →
Semántica de sincronización ZFS NFS: Por qué los clientes cambian la seguridad de tus escrituras

Deja un comentario