Vous lancez un déploiement. Le conteneur redémarre. Puis redémarre encore. Les logs affichent le classique : Text file busy.
Parfois ça marche au deuxième essai, parfois après cinq, parfois seulement quand vous regardez.
Cette erreur est un petit message avec une longue queue : Linux vous indique que vous avez tenté de modifier ou d’exécuter quelque chose
que le noyau considère encore comme « en cours d’utilisation ». En production, cela se traduit par des redémarrages instables, des échecs de déploiement mystérieux,
et des équipes qui blâment Docker alors que le vrai coupable est la façon dont vous envoyez les fichiers.
Ce que signifie réellement « Text file busy » (et pourquoi Docker est accusé)
Sous Linux, Text file busy correspond généralement à ETXTBUSY : vous avez tenté une opération sur un fichier exécutable
(historiquement appelé « text » parce que le segment texte contient les instructions) alors qu’il est en cours d’exécution ou autrement verrouillé par le noyau d’une manière qui bloque votre action.
Dans l’univers des conteneurs, cela survient pendant les déploiements parce que nous mélangeons deux mondes :
- Images quasi immuables (bien) : les couches sont adressées par contenu, construites une fois, exécutées plusieurs fois.
- Montages bind mutables (dangereux) : des fichiers de l’hôte apparaissent en direct dans le conteneur.
Le schéma d’échec classique : un job CI, un script de déploiement ou un sidecar met à jour un binaire ou un script d’entrée dans un chemin monté en bind
alors qu’un conteneur existant est encore en train de démarrer, d’arrêter ou de redémarrer. Le noyau refuse l’opération au pire moment.
Docker n’est que le messager. Docker a aussi le défaut pratique d’être le messager que l’on insulte, parce que c’est commode et qu’il ne peut pas répondre.
La correction qui élimine la volatilité est ennuyeuse et absolue : ne mettez jamais à jour des exécutables en place dans un chemin que
pourrait exécuter un conteneur en cours d’exécution. Produisez un nouveau fichier, puis basculez atomiquement ce que « current » pointe (typiquement via un échange de symlink ou de montage bind),
ou arrêtez d’utiliser les bind mounts pour les exécutables.
La citation à coller près de vos scripts de déploiement
« L’espoir n’est pas une stratégie. » (idée paraphrasée courante dans les cercles ops/fiabilité)
Si votre déploiement repose sur « espérons que l’ancien processus se termine avant qu’on écrase le fichier », vous ne déployez pas. Vous jouez au casino avec un meilleur branding.
Blague n°1 : Les conteneurs sont du bétail, mais celui qui contient votre script de déploiement est toujours un animal de compagnie. Il a un nom, et il mord.
Playbook de diagnostic rapide
C’est la séquence « j’ai 10 minutes avant que le responsable release n’ouvre un ticket ». L’objectif n’est pas la pureté philosophique.
L’objectif est d’identifier si vous êtes face à (a) une mutation d’un bind mount, (b) un comportement d’overlayfs/couche, (c) une course d’arrêt, ou (d) autre chose.
Première étape : trouver le fichier exact qui est « busy »
- Depuis les logs, extrayez le chemin :
/app/bin/service,/entrypoint.sh,/usr/local/bin/foo. - Si les logs ne l’indiquent pas, lancez la commande qui échoue avec
strace(voir les tâches ci-dessous) pour capturer quel fichier retourneETXTBUSY.
Deuxième étape : déterminer si ce chemin est un bind mount
docker inspect→.Mountspour le conteneur.- Si c’est un bind mount, vous avez probablement trouvé le coupable.
Troisième étape : identifier quel processus l’a encore ouvert/exécuté
- Utilisez
lsofoufusersur l’hôte pour le chemin côté hôte. - Si c’est dans le système de fichiers du conteneur (overlay2), vérifiez les processus en cours et leurs chemins d’exécutables.
Quatrième étape : décider du mode d’échec
- Le script de déploiement a écrasé un exécutable en cours d’exécution → corriger la méthode de déploiement (bascule atomique), arrêter les éditions in-place.
- L’entrypoint est monté en bind et remplacé → arrêtez de monter les scripts d’entrypoint ; intégrez-les dans l’image ou montez-les en lecture seule avec commutation de release.
- L’arrêt prend trop de temps → gérez
SIGTERM, ajoutez un preStop/wait, augmentez la tolérance, ne tuez pas de force trop tôt. - Processus qui se met à jour lui-même (oui, ça existe encore) → supprimez la mise à jour auto ; expédiez une nouvelle image à la place.
Cinquième étape : appliquer une atténuation fiable pendant que vous préparez la vraie correction
- Arrêtez d’écraser ; écrivez dans un nouveau chemin puis renommez/échangez le symlink.
- Montez les exécutables en lecture seule.
- Désactivez temporairement « restart always » pour éviter une tempête de redémarrages qui masque la cause racine.
Causes profondes : 8 façons d’obtenir ETXTBUSY
1) Écrasement in-place d’un binaire en cours d’exécution (bind mount ou volume partagé)
Quelqu’un fait cp new-binary /srv/app/bin/service pendant que l’ancien service tourne, ou un conteneur est en train de démarrer.
Linux autorise de nombreuses opérations sur des fichiers ouverts, mais remplacer un binaire en cours d’exécution peut déclencher ETXTBUSY selon la séquence et le système de fichiers.
Le message est votre seul avertissement que votre modèle de déploiement vit dangereusement.
2) Remplacement d’un script d’entrypoint en cours d’exécution
Les scripts shell sont aussi des exécutables. Si votre conteneur démarre avec /entrypoint.sh depuis un bind mount, et que votre déploiement met à jour ce fichier,
vous pouvez obtenir « text file busy » pendant le démarrage — exactement quand votre orchestrateur effectue beaucoup de redémarrages, checks de santé et manque de patience.
3) Le CI/CD écrit dans un répertoire qui est aussi le répertoire runtime
L’approche « simple » : un job construit des artefacts et les dépose dans /srv/app/current. Un autre job redémarre le conteneur.
Si ces étapes se chevauchent ou se relancent, vous avez créé une condition de course avec la production comme arbitre.
4) Deux conteneurs partagent le même chemin hôte et déploient de manière asynchrone
Un conteneur exécute encore l’ancien code depuis un bind mount partagé ; un autre conteneur met à jour ce montage.
Félicitations : vous avez implémenté une contention de verrou distribué en n’utilisant que des scripts shell.
5) Politiques de redémarrage agressives créent un DoS auto-infligé
restart: always va bien quand la panne est rare. Quand la panne est déclenchée par une étape de déploiement qui se répète,
vous obtenez une boucle de redémarrage serrée. Cette boucle augmente les chances de collision avec la fenêtre de remplacement du fichier.
L’erreur devient « instable » parce que le timing change.
6) Particularités d’overlayfs quand vous mutilez des chemins « censés être immuables »
Le driver overlay2 de Docker est conçu pour des couches copy-on-write. La plupart du temps il se comporte comme un système de fichiers normal.
Mais quand vous essayez de faire des choses astucieuses — comme hot-swapping d’exécutables dans des couches modifiables pendant le démarrage — vous vous appuyez sur des subtilités :
whiteouts, copy-up, et sémantiques par couche. Vous ne verrez pas ETXTBUSY à chaque fois, mais vous le verrez au pire moment.
7) Malentendus sur l’atomicité : rename est atomique, copy ne l’est pas
Les gens disent « on met à jour atomiquement » puis vous montrent un cp. Copier un fichier par-dessus un autre fichier n’est pas atomique de la manière nécessaire.
Un rename sur le même système de fichiers est atomique ; un copy suivi d’un overwrite est une invitation à des lectures partielles et des erreurs bizarres.
8) Arrêts longs + kill forcé + redémarrage immédiat
Si votre service met du temps à se terminer et que votre orchestrateur le tue trop tôt, le processus peut être encore présent lors de la tentative de démarrage suivante
(ou le système de fichiers a encore des références d’exécution). Les boucles serrées amplifient cela.
Souvent la correction n’est pas « sleep 5 » (bien que ça « marche »), mais rendre l’arrêt déterministe et les étapes de déploiement non chevauchantes.
Blague n°2 : Ajouter sleep 10 pour corriger une condition de course revient à réparer une fuite de toit en achetant une pluie plus bruyante.
La correction durable : ne plus modifier les exécutables en place
Si vous retenez une seule chose : déployez en changeant des pointeurs, pas en mutuant des fichiers en direct.
Cela veut dire répertoires de release, symlinks, et montages en lecture seule, ou construire une nouvelle image et remplacer les conteneurs.
Vous voulez que le runtime voie un artefact complet et cohérent, à chaque fois.
À quoi ressemble le « bon »
- Les artefacts sont immuables : un binaire ou un script est écrit une fois, puis jamais modifié.
- L’activation est atomique : vous passez de la release A à la release B avec une opération atomique (renommer un symlink, mettre à jour la cible d’un bind mount).
- Le rollback est la même opération : revenir au pointeur précédent.
- Les conteneurs ne partagent pas d’exécutables mutables : s’ils partagent quelque chose, c’est des données, pas du code.
Les deux modèles de déploiement qui tiennent vraiment
Pattern A : Intégrer les exécutables dans l’image (recommandé)
L’image du conteneur est l’artefact. Déployer signifie : pull de la nouvelle image, démarrage du nouveau conteneur, arrêt de l’ancien conteneur.
Pas de bind mounts pour /app/bin. Pas de « patchage à chaud » à l’intérieur du conteneur. C’est pour cela que Docker a été conçu.
Pattern B : Répertoires de release + échange atomique de symlink (quand vous devez utiliser des bind mounts)
Parfois vous êtes coincé : contraintes réglementaires, artefacts énormes, builds isolés, hypothèses runtime legacy.
D’accord. Faites-le comme des adultes :
- Écrivez la nouvelle release dans
/srv/app/releases/2026-01-03_120501/. - Vérifiez-la (checksums, permissions, smoke test).
- Mettez à jour de façon atomique le symlink
/srv/app/currentpour pointer vers la nouvelle release. - Redémarrez les conteneurs qui montent
/srv/app/currenten lecture seule.
L’essentiel est que /srv/app/current change instantanément comme pointeur ; le contenu des répertoires de release ne change jamais.
Cela élimine le « exécutable à moitié copié » et réduit fortement le « text file busy » parce que vous n’écrasez pas le fichier en cours d’exécution.
Si quelque chose exécute encore l’ancien binaire, il continue d’exécuter l’ancien inode. Les nouveaux conteneurs démarrent sur le nouvel inode.
C’est ainsi que vous achetez du bon sens grâce aux sémantiques du système de fichiers.
Petits choix de durcissement importants
- Montez le code en lecture seule dans les conteneurs. Si quelque chose tente de le muter, cela échoue bruyamment.
- Ne remplacez jamais en bind mount
/usr/local/binsauf si vous aimez l’archéologie. - Rendez les entrypoints immuables (intégrez-les dans l’image). Si vous devez les monter, montez un chemin versionné et basculez via symlink.
- Contrôlez les redémarrages : évitez les boucles infinies qui masquent de vraies pannes ; utilisez du backoff et de l’alerte.
Tâches pratiques : 12+ commandes qui expliquent ce qui se passe
Voici les commandes à lancer quand le déploiement échoue et que des gens suggèrent « redémarrez juste le nœud ».
Chaque tâche inclut : commande, sortie d’exemple, ce que cela signifie, et la décision à prendre.
Tâche 1 : Confirmer la signature d’échec dans les logs du conteneur
cr0x@server:~$ docker logs --tail=80 api-1
exec /app/bin/api: text file busy
Ce que cela signifie : Le noyau a refusé un execve() de /app/bin/api (ou un shell l’a invoqué) avec ETXTBUSY.
Décision : Identifiez si /app/bin/api provient d’une couche d’image ou d’un montage. Si c’est un montage, arrêtez de le mettre à jour en place.
Tâche 2 : Inspecter les mounts et trouver rapidement les bind mounts
cr0x@server:~$ docker inspect api-1 --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/app/current","Destination":"/app","Mode":"ro","RW":false,"Propagation":"rprivate"}]
Ce que cela signifie : /app est un bind mount depuis le chemin hôte /srv/app/current. Si les scripts de déploiement modifient des fichiers sous cet arbre, vous pouvez entrer en course avec l’exécution.
Décision : Vérifiez si /srv/app/current est un symlink vers des releases versionnées. Sinon, implémentez-le.
Tâche 3 : Vérifier si « current » est un symlink (et où il pointe)
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
Ce que cela signifie : Bon signe : current est un pointeur. Si le déploiement met à jour le symlink de façon atomique, les conteneurs voient des coupures nettes.
Décision : Assurez-vous que le déploiement écrit dans un nouveau répertoire de release et ne modifie jamais le contenu de la release pointée après activation.
Tâche 4 : Repérer un comportement de « copie in-place » dans les scripts de déploiement
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
Ce que cela signifie : Quelqu’un copie directement dans l’arbre live. C’est la course.
Décision : Changez le déploiement pour mettre en scène dans un nouveau répertoire puis faire un symlink swap, ou construisez une nouvelle image et redéployez.
Tâche 5 : Identifier qui tient le fichier ouvert (côté hôte)
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
Ce que cela signifie : Le PID 23144 exécute le binaire (le mapping txt). Écraser cet inode est exactement comment vous déclenchez ETXTBUSY et pire.
Décision : N’écrasez pas ce fichier. Déployez un nouvel inode (nouveau chemin) et basculez via symlink ; ou arrêtez proprement le processus avant tout remplacement.
Tâche 6 : Utiliser fuser pour confirmer les processus utilisant l’exécutable
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
Ce que cela signifie : Même résultat, outil différent : un processus exécute le fichier.
Décision : Corrigez le déploiement pour éviter le remplacement de fichier ; ne « réessayez pas jusqu’à ce que ça marche ».
Tâche 7 : Voir la vue du conteneur sur l’exécutable et confirmer qu’elle correspond au mount
cr0x@server:~$ docker exec api-1 readlink -f /app/bin/api
/srv/app/current/bin/api
Ce que cela signifie : Le conteneur exécute depuis l’arbre bind-monté.
Décision : Traitez /srv/app comme un stockage de code de production. Versionnez-le sur le disque ; montez-le en lecture seule ; basculez les pointeurs atomiquement.
Tâche 8 : Prouver si le fichier est modifié pendant le déploiement (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
Ce que cela signifie : Quelque chose modifie api en place. C’est votre coupable évident.
Décision : Supprimez la mutation in-place. Si vous devez mettre à jour, écrivez api.new, vérifiez, puis renommez ou échangez le symlink.
Tâche 9 : Vérifier le comportement de boucle de redémarrage 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
Ce que cela signifie : Le conteneur oscille. Cela amplifie les courses de timing et rend les logs plus difficiles à interpréter.
Décision : Arrêtez temporairement le conteneur pour stabiliser le système, puis corrigez la méthode de déploiement ; ou ajoutez du backoff dans l’orchestrateur.
Tâche 10 : Inspecter l’état du conteneur pour la dernière erreur
cr0x@server:~$ docker inspect api-1 --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}'
restarting 1
Ce que cela signifie : Docker n’a pas capturé une chaîne « error » distincte ici ; vous avez besoin des logs + du traçage des appels système pour les détails.
Décision : Utilisez strace sur le chemin exec qui échoue ou reproduisez dans un conteneur one-shot pour capturer l’origine d’ETXTBUSY.
Tâche 11 : Reproduire avec un run one-shot qui affiche l’exec qui échoue
cr0x@server:~$ docker run --rm -v /srv/app/current:/app:ro api:prod /app/bin/api --version
bash: /app/bin/api: Text file busy
Ce que cela signifie : Même un run one-shot propre rencontre le problème. Cela suggère que le chemin hôte est activement modifié ou que le fichier est dans un état intermédiaire étrange.
Décision : Arrêtez la pipeline de déploiement, vérifiez l’intégrité du fichier (taille, checksum, permissions), et confirmez que personne n’écrit dans le chemin live.
Tâche 12 : Confirmer si le fichier est remplacé via rename (bien) ou écrasé (mal)
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"
Ce que cela signifie : Vous avez attrapé cp en train d’écrire directement sur l’exécutable. Ce n’est pas atomique et cela entre en collision avec l’exécution.
Décision : Remplacez le « copy over » par « écrire un nouveau fichier puis renommer » ou « mettre en scène la release puis symlink swap ».
Tâche 13 : Vérifier le comportement de renommage atomique dans votre répertoire de déploiement
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
Ce que cela signifie : mv -T traite la cible comme un fichier ; -f force le remplacement. C’est un pattern commun et fiable pour les mises à jour atomiques de symlink sur Linux.
Décision : Standardisez cela comme étape d’activation. Pas de copies partielles dans current.
Tâche 14 : Confirmer que le mount est en lecture seule dans le conteneur
cr0x@server:~$ docker exec api-1 sh -lc 'mount | grep " /app "'
/dev/sda1 on /app type ext4 (ro,relatime,errors=remount-ro)
Ce que cela signifie : Le conteneur ne peut pas modifier le code sous /app. C’est bien : cela empêche un comportement d’auto-modification et renvoie la faute au pipeline de déploiement où elle appartient.
Décision : Gardez-le en lecture seule. Si quelque chose casse parce qu’il s’attendait à écrire là, corrigez l’app pour écrire dans un volume de données.
Tâche 15 : Valider le timing d’arrêt gracieux pour éviter des exécutions qui se chevauchent
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
Ce que cela signifie : Le processus se termine proprement dans la période de grâce. S’il ne l’avait pas fait, vous auriez vu un kill forcé et un risque plus élevé de collisions entre redémarrages et étapes de déploiement.
Décision : Si l’arrêt est lent, corrigez le handling des signaux, ajoutez des hooks preStop, et ajustez les timeouts. Ne compensez pas avec des « retries » de déploiement.
Tâche 16 : Utiliser strace pour confirmer l’appel système qui renvoie 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)
Ce que cela signifie : Pas de conjecture. Le noyau a retourné ETXTBUSY sur execve de ce chemin.
Décision : Considérez cela comme un bug du cycle de vie de l’artefact. Changez la façon dont le fichier est produit et activé ; ne « tunez » pas Docker.
Trois mini-récits du monde de l’entreprise
Mini-récit n°1 : L’incident causé par une mauvaise hypothèse
Une fintech de taille moyenne exécutait une stack Docker Compose sur quelques VM costaudes. Ils déployaient un binaire Go et quelques scripts via un bind mount :
/srv/finapp/current monté dans /app. L’hypothèse était simple et fausse : « Linux vous permet de remplacer des fichiers pendant qu’ils sont utilisés. »
Le job de déploiement faisait cp d’un nouveau /srv/finapp/current/bin/api et lançait immédiatement docker compose up -d.
Les jours calmes ça fonctionnait. Les jours chargés, certains conteneurs redémarraient, tombaient sur Text file busy, et oscillaient. Le pager s’est déclenché.
Les gens accusaient « Docker d’être instable », parce que l’erreur apparaissait au démarrage du conteneur, pas pendant la copie.
Le post-mortem a montré le vrai mode de défaillance : plusieurs instances de l’app exécutaient bin/api depuis le bind mount,
tandis que le déploiement le remplaçait en place. Parfois la copie et l’exec entraient en collision. Parfois la copie écrivait partiellement et le exec suivant
obtenait une erreur différente. Ils avaient construit une course dépendant du timing, de l’ordonnancement CPU, et d’une bonne dose de mauvaise foi.
La correction n’a rien d’exotique. Ils ont mis en scène les artefacts dans /srv/finapp/releases/<id>, les ont vérifiés, et ont échangé atomiquement
current via mv -Tf. Ils ont aussi monté /app en lecture seule pour être sûrs qu’aucun conteneur ne puisse le muter.
Le déploiement suivant a été ennuyeux, ce qui est le ton émotionnel correct pour un déploiement.
Mini-récit n°2 : L’optimisation qui s’est retournée
Une société ad-tech voulait des déploiements plus rapides. Fatigués de construire des images, ils ont tenté « l’injection d’artefacts » :
construire une fois sur l’hôte, puis le bind monter dans le conteneur. Ils ont aussi activé une politique de redémarrage agressive pour que les services « guérissent ».
C’était rapide. C’était aussi une excellente manière de transformer une course de déploiement en festival de redémarrages dans tout le cluster.
Quand un déploiement démarrait, les conteneurs redémarraient rapidement, certains attrapaient le fichier alors qu’il était en plein update, et plusieurs ont pris ETXTBUSY.
La politique de redémarrage relançait, ce qui augmentait la probabilité de collision avec la fenêtre de déploiement. Boucle de rétroaction atteinte.
L’équipe « a corrigé » en ajoutant des appels sleep entre copie et redémarrage, puis en ajoutant plus de sleep quand le premier n’était pas assez long.
Les déploiements ont ralenti. Les échecs sont devenus plus rares. Puis un jour particulièrement occupé a changé le timing.
L’erreur est revenue comme une allergie saisonnière.
La vraie correction a été d’arrêter d’optimiser la mauvaise chose. Ils sont revenus à la construction d’images pour la production, et n’ont utilisé les bind mounts qu’en dev.
Pour les quelques services nécessitant encore des assets montés sur l’hôte, ils ont utilisé des répertoires de release et un symlink swap. Le temps de déploiement a un peu augmenté.
Le temps passé en incident a fortement diminué. C’est le compromis souhaitable.
Mini-récit n°3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Un fournisseur SaaS dans la santé avait une politique stricte d’« artefacts immuables ». Les ingénieurs s’en plaignaient comme on se plaint des ceintures de sécurité.
Chaque déploiement produisait un répertoire de release versionné avec checksums et manifest. L’activation était un symlink swap. Le rollback était identique.
Un vendredi, un hic de stockage a fait que le job de déploiement a réessayé en plein staging. Un second job a démarré avant que le premier ne finisse.
Cela aurait été un incident classique de « text file busy », car les deux jobs visaient la même app. Mais ils ne visaient pas le même répertoire.
Chaque run de staging écrivait dans un nouveau chemin de release unique.
L’étape d’activation utilisait un lock et une mise à jour atomique de current. Un seul job a gagné. L’autre a échoué vite et bruyamment.
Le service n’a jamais vu d’artefacts partiels. Les conteneurs ont redémarré exactement une fois. Personne n’a appris un nouveau message d’erreur ce jour-là.
Le postmortem a été court. La correction a été de rendre le lock plus explicite et d’améliorer l’alerte sur la contention de déploiement.
La pratique qui les a sauvés n’était pas de l’héroïsme. C’était une bonne hygiène du système de fichiers appliquée de manière cohérente, ce qui est la vraie source de fiabilité.
Erreurs courantes : symptôme → cause → correction
1) Symptom : « Text file busy » seulement pendant le déploiement, puis ça disparaît
Cause : Mise à jour in-place des artefacts en collision avec les redémarrages du conteneur ; course dépendante du timing.
Correction : Mettre en scène dans un répertoire versionné ; basculer atomiquement le symlink ; monter le code runtime en lecture seule.
2) Symptom : Cela arrive plus quand le trafic est élevé
Cause : Arrêt lent ou démarrage plus long augmente la fenêtre de chevauchement ; les boucles de redémarrage amplifient les collisions.
Correction : Rendre l’arrêt déterministe (gestion SIGTERM), augmenter la période de grâce, ajouter du backoff ; éviter les tempêtes de redémarrage pendant le déploiement.
3) Symptom : Un seul nœud affiche le problème
Cause : Comportement de script de déploiement spécifique au nœud, système de fichiers différent (NFS vs ext4 local), ou options de mount différentes.
Correction : Comparez les types et options de mount, standardisez le mécanisme de déploiement, évitez d’exécuter depuis des partages réseau quand c’est possible.
4) Symptom : « Text file busy » pour entrypoint.sh ou scripts de démarrage
Cause : L’entrypoint est monté en bind et mis à jour ; ou la gestion de configuration le réécrit.
Correction : Intégrez l’entrypoint dans l’image ; ou versionnez les scripts et basculez les pointeurs, ne les écrasez jamais.
5) Symptom : Le déploiement utilise rsync et échoue quand même
Cause : rsync met à jour les fichiers in-place par défaut ; le comportement des fichiers temporaires/rename dépend des flags.
Correction : Utilisez un répertoire de staging ; ou rsync vers un nouveau chemin de release ; puis symlink swap. Ne rsyncez pas dans « current ».
6) Symptom : « Mais on utilise rename, ça devrait être atomique »
Cause : Rename est atomique uniquement sur le même système de fichiers et uniquement pour l’opération de rename ; votre processus peut encore être en train d’écraser ou de copier.
Correction : Assurez-vous que staging et activation se produisent sur le même système de fichiers ; utilisez mv -Tf sur les symlinks ; évitez les déplacements cross-filesystem.
7) Symptom : Le conteneur échoue avec « permission denied » après que vous l’ayez « corrigé »
Cause : Le nouveau répertoire de release a une mauvaise propriété/exécutable manquant ; le montage en lecture seule révèle un packaging négligé.
Correction : Définissez les permissions correctes dans l’étape de build ; vérifiez avec stat ; ajoutez un smoke test pré-activation.
8) Symptom : Ça n’arrive que dans Kubernetes, pas en local
Cause : Hooks de lifecycle, reprogrammation rapide, et readiness/liveness provoquent un timing plus agressif. De plus, les volumes partagés sont plus courants.
Correction : Évitez les volumes exécutables partagés ; utilisez des images ; si vous devez utiliser des volumes, employez des chemins versionnés et des mises à jour atomiques de pointeurs avec proper terminationGracePeriodSeconds.
Listes de contrôle / plan étape par étape
Checklist : Contention immédiate (aujourd’hui)
- Arrêtez la tempête de redémarrages : désactivez temporairement les redémarrages automatiques ou réduisez l’échelle du service en panne.
- Gelez les jobs de déploiement qui écrivent dans le chemin runtime.
- Identifiez le chemin occupé depuis les logs ou
strace. - Vérifiez si ce chemin est bind-mounted ; si oui, considérez-le comme suspect principal.
- Utilisez
lsof/fusersur l’hôte pour confirmer quel processus l’exécute. - Si nécessaire, arrêtez le service proprement avant d’autres modifications. Évitez les boucles de kill forcé.
Checklist : Remédiation durable (cette semaine)
- Décidez votre modèle d’artefact :
- Préférence : intégrer les binaires dans l’image et redéployer les conteneurs.
- Fallback : répertoires de release versionnés + symlink swap.
- Changez le déploiement pour mettre en scène les artefacts dans un répertoire unique :
- Écrire :
/srv/app/releases/<release-id>/ - Ne jamais écrire :
/srv/app/current/
- Écrire :
- Ajoutez des vérifications avant l’activation :
- Validation checksum/taille
- Vérification des permissions (
+x) - Test basique d’exécution (
--versionou--help)
- Activez via un switch de pointeur atomique :
ln -sfn ... current.newmv -Tf current.new current
- Montez le chemin de code en lecture seule dans Docker/Compose/Kubernetes.
- Assurez une fermeture gracieuse :
- Gérer SIGTERM
- Définir des timeouts d’arrêt raisonnables
Checklist : Garde-fous (ce trimestre)
- Ajoutez une vérification CI qui échoue si des scripts de déploiement copient dans « current ».
- Ajoutez de l’audit/inotify pendant les canaris pour garantir qu’aucune modification in-place n’a lieu.
- Standardisez les chemins d’artefacts et les patterns de montage entre services.
- Enregistrez la contention de déploiement : si deux déploiements se chevauchent, échouez-en un immédiatement avec une erreur claire.
- Faites des rollbacks une fonctionnalité de première classe : conservez les releases précédentes sur disque ; revenez via symlink.
Faits intéressants et contexte historique
- ETXTBUSY est un héritage d’Unix avec des conséquences modernes : le nom vient de « text segment », le mapping du code exécutable dans les premiers Unix.
- Linux peut unlinker des exécutables en cours : un processus peut continuer à tourner même si son exécutable est supprimé, car il garde une référence inode ouverte.
- Le renommage atomique est l’une des garanties les plus fortes du système de fichiers : sur les systèmes POSIX, renommer au sein du même système de fichiers est atomique, d’où l’efficacité des swaps de symlink.
- Les filesystems copy-on-write ont changé les attentes : overlayfs et les images en couches encouragent l’immuabilité, mais les bind mounts réintroduisent la mutabilité là où ça fait mal.
- Les scripts shell peuvent aussi déclencher ETXTBUSY : tout ce qui est exécuté via
execvepeut être « busy », pas seulement les binaires compilés. - Les boucles de redémarrage masquent les causes profondes : les orchestrateurs réessaient rapidement ; les logs tournent ; le premier message d’erreur disparaît sous une pile de redémarrages identiques.
- NFS et filesystems réseau apportent leur propre saveur : la consistance close-to-open et le cache peuvent produire un comportement dépendant du timing qui ressemble à ETXTBUSY ou des mises à jour partielles.
- Le « hot patching » est généralement un mauvais signe de déploiement : les services qui se mettent à jour eux-mêmes étaient plus courants avant les conteneurs ; avec les images, c’est presque toujours une mauvaise idée.
- Les releases basées sur symlink préexistent aux conteneurs : le pattern existe depuis des décennies en hébergement web et serveurs d’applications parce qu’il correspond au comportement des noyaux et systèmes de fichiers.
FAQ
1) « Text file busy » est-ce un bug Docker ?
Presque jamais. C’est le noyau qui refuse une opération (généralement execve ou une mise à jour de fichier) en raison de la façon dont le fichier est utilisé.
Docker est simplement l’endroit où vous le voyez.
2) Pourquoi ça n’arrive que parfois ?
Les races dépendent de l’ordonnancement. La charge CPU, la latence IO, le timing des redémarrages, et le chevauchement des jobs de déploiement changent tous la fenêtre.
« Parfois » est exactement la façon dont les conditions de course se manifestent.
3) Puis-je le corriger en ajoutant des retries ou des sleeps ?
Vous pouvez masquer le problème. Vous ne le corrigerez pas. Vous pariez que le timing sera plus clément la prochaine fois, ce qui n’est pas un contrat que vous pouvez garantir.
La vraie correction est d’arrêter d’écraser les exécutables en place et d’activer les nouvelles releases de façon atomique.
4) Monter le répertoire en lecture seule aide-t-il ?
Oui, comme garde-fou. Cela empêche les conteneurs de modifier leur propre code et révèle rapidement les hypothèses incorrectes.
Cela ne corrige pas un script de déploiement côté hôte qui écrase encore les fichiers, donc combinez-le avec un staging de release approprié.
5) Que faire si je dois utiliser des bind mounts pour le code (contraintes legacy) ?
Utilisez des répertoires de release versionnés et un pointeur symlink comme /srv/app/current. Ne copiez jamais dans current.
Montez current en lecture seule dans les conteneurs. Basculez le symlink de façon atomique.
6) Pourquoi changer un symlink aide si des processus tournent encore sur l’ancien code ?
Parce que les processus exécutent des inodes, pas des chemins. Un processus en cours continue d’utiliser son inode déjà ouvert.
Les nouveaux processus résolvent le symlink vers un nouvel inode. Vous évitez de modifier l’inode dont dépend un processus en cours.
7) Est-ce que cela arrive avec overlay2 même sans bind mounts ?
Ça peut arriver, mais c’est moins courant. La plupart des problèmes ETXTBUSY en déploiement proviennent de la mutation de fichiers montés depuis l’hôte.
Si vous modifiez des fichiers à l’intérieur d’un conteneur à runtime (surtout des exécutables), vous recréez le même problème à l’intérieur d’overlayfs.
8) Comment prouver quel processus est responsable ?
Utilisez lsof ou fuser sur le chemin hôte, et confirmez le mapping de mount du conteneur avec docker inspect.
Si nécessaire, utilisez strace pour attraper le execve qui retourne ETXTBUSY.
9) rsync est-il sûr si j’utilise –inplace ou –delay-updates ?
--inplace est activement dangereux pour des exécutables live. --delay-updates est mieux, mais le pattern le plus sûr reste :
rsync dans un tout nouveau répertoire de release, vérifiez, puis changez le pointeur.
10) Quelle est la correction structurelle la plus rapide et la moins conflictuelle ?
Gardez votre structure de bind mounts existante, mais changez le déploiement pour créer un nouveau répertoire de release et faire un symlink swap atomique.
C’est peu impactant, à forte valeur, et facile à auditer.
Conclusion : ce qu’il faut changer lundi matin
« Text file busy » pendant les déploiements Docker n’est pas un mystère cosmique. C’est votre système de fichiers qui vous dit que votre méthode de déploiement est dangereuse.
La correction n’est pas non plus mystérieuse : arrêtez de muter les exécutables en place, et activez les releases de façon atomique.
Prochaines étapes qui rapportent immédiatement :
- Trouvez le chemin qui déclenche ETXTBUSY et confirmez s’il est bind-mounted.
- Éliminez les copies in-place dans le répertoire runtime live.
- Adoptez soit des déploiements basés image, soit des répertoires de release + symlink swap.
- Montez le code en lecture seule et rendez les entrypoints immuables.
- Réduisez les tempêtes de redémarrage pour que les pannes soient visibles, et non diluées par les retries.
L’objectif n’est pas de « ne plus jamais voir ETXTBUSY ». L’objectif est de construire des déploiements qui ne dépendent pas de la chance, du timing, ou de la phase de la lune.
Votre futur vous sera encore en astreinte. Faites-lui une faveur.