Chaque modification de nos sites et de nos outils passe par une intégration continue (CI) : une machine reconstruit le projet, lance les tests et ne laisse partir en ligne que ce qui passe. Pour un chef d’entreprise, la CI se résume à une question simple : combien de temps entre « le correctif est prêt » et « le correctif est en ligne » ? Chaque minute d’attente est une minute pendant laquelle un formulaire cassé reste cassé.

Nous faisons tourner cette CI sur notre propre serveur, avec des runners auto-hébergés (les « ouvriers » qui exécutent les tâches de GitHub Actions). Le 11 octobre 2026, nous avons déplacé leurs fichiers temporaires du disque vers la mémoire vive. Voici ce que nous avons mesuré, y compris ce qui ne marche pas.

Le point de départ : un disque qui attend

Le serveur de CI est un VPS cloud de 12 vCPU et 48 Go de mémoire vive, sur disque NVMe, avec une soixantaine de runners enregistrés. Un sémaphore limite le nombre de tâches simultanées, réglé entre 8 et 16 places selon la période.

Sur le papier, un disque NVMe est rapide. Sauf que les tests de base de données écrivent de façon synchrone : PostgreSQL et SQLite demandent au système de confirmer que chaque écriture est bien posée sur le disque avant de continuer (l’appel fsync, décrit dans la documentation de PostgreSQL citée en source). Nous avons donc mesuré ce cas précis, avec la commande dd en mode synchrone, sur ce serveur :

  • sur le disque : 163 ko/s ;
  • dans la mémoire vive (/dev/shm) : 437 Mo/s.

Selon ce relevé, l’écart dépasse un facteur mille. Quand plusieurs tâches de CI attendent chacune leurs fsync sur le même disque, elles font la queue.

Ce que nous avons changé

Trois réglages, tous réversibles :

  1. Un dossier temporaire par runner, en mémoire vive. Chaque runner reçoit son propre TMPDIR dans un tmpfs (un système de fichiers qui vit en mémoire, documenté par le noyau Linux). Il est vidé au début de chaque tâche, pour qu’un test ne retrouve jamais les restes du précédent.
  2. Les volumes Docker des bases de test sur tmpfs. La base PostgreSQL démarrée pour les tests pgTAP n’écrit plus sur le disque.
  3. Un plancher de mémoire dans le sémaphore. Une tâche n’est admise que s’il reste assez de mémoire libre. Sans ce garde-fou, la mémoire vive se remplit et le serveur coupe des tâches au hasard.

Rien n’a changé dans les tests eux-mêmes : mêmes runners, même serveur, même workflow.

Les chiffres, avant et après

Nous avons mesuré une étape précise : le job de base de données de notre application de gestion, qui démarre PostgreSQL, rejoue toutes les migrations puis lance les tests pgTAP. Les durées viennent de l’API de GitHub Actions (heure de début et de fin de chaque étape).

  • Avant : du 10 octobre 14 h au 11 octobre 17 h 30 (UTC), disque seulement, 14 exécutions.
  • Après : à partir du 11 octobre 19 h 40 (UTC), mémoire vive, 12 exécutions.
  • La fenêtre de bascule entre les deux est exclue.

Selon nos relevés, par niveau de charge du serveur au démarrage de l’étape :

Charge du serveur Avant (médiane, pire cas) Après (médiane, pire cas) Écart selon nos relevés
Serveur occupé (charge 5 à 13) 201 s, 386 s 143 s, 171 s -29 %, pire cas -56 %
Serveur calme (charge sous 5) 108,5 s, 121 s 120 s, 133 s pas de gain mesurable
Toutes charges confondues 181 s, 393 s 124 s, 171 s -31 %

Lecture honnête de ces relevés : le gain n’existe que lorsque le serveur est chargé. Sur un serveur calme, le disque suffit et l’écart reste dans le bruit de mesure. Sous charge, les écritures synchrones de plusieurs tâches se marchent dessus sur le disque. En mémoire vive, elles ne coûtent presque rien, et surtout le pire cas s’effondre : de plus de six minutes à moins de trois.

C’est ce pire cas qui compte pour l’entreprise. Une CI dont la durée varie du simple au triple oblige à surveiller. Une CI prévisible se laisse travailler seule.

La réserve qu’il faut écrire

Ce n’est pas une expérience contrôlée. Pendant la fenêtre « avant », le sémaphore est passé de 8 à 16 places. Nous publions les chiffres avec cette réserve plutôt que de les arrondir à notre avantage.

Le piège qui aurait faussé toute la conclusion

Sur la même fenêtre, une autre étape semblait passer de 135 s à 826 s de médiane, selon les mêmes relevés de l’API. À lire le tableau seul, la mémoire vive aurait rendu ce job six fois plus lent.

Le journal du job racontait autre chose : la tâche avait attendu 731 secondes un verrou exclusif propre à ce workflow, posé pour éviter que deux tests touchent la même ressource. Rien à voir avec le disque. Nous avons donc retiré l’attente de verrou de chaque mesure, en la lisant dans les journaux, avant de comparer.

La règle que nous en tirons : toujours lire les journaux avant de conclure. Une moyenne ne dit pas pourquoi une tâche a été lente.

Le gain le plus gros n’était pas la mémoire vive

En préparant cette mesure, nous avons trouvé une perte bien plus lourde, et bien plus simple à corriger. L’action setup-node de GitHub propose une option de cache (cache: npm). Sur un runner hébergé par GitHub, elle fait gagner du temps. Sur un runner auto-hébergé, elle télécharge à chaque tâche une archive depuis le stockage de GitHub, alors que le cache npm est déjà présent sur le disque local du serveur.

Depuis notre serveur, ce téléchargement plafonnait autour de 5 Mo/s. Selon nos relevés du 11 octobre sur cinq dépôts :

Projet Étape setup-node avant Après Durée totale du job avant Après
Site A, test et build 465 à 1 204 s 1 s 573 à 1 552 s 140 s
Site B, validation 377 à 1 165 s 1 s 580 à 1 347 s 39 et 104 s
Application D, frontend 178 à 1 182 s 1 s 682 à 2 466 s 664 s

Une ligne retirée par fichier de workflow, 48 dépôts corrigés le même soir, et une étape qui prenait de 6 à 20 minutes tombe à 1 ou 2 secondes. Un contrôle automatique empêche désormais l’option de revenir.

Un détail technique à connaître : selon la documentation de setup-node, les versions récentes réactivent ce cache d’elles-mêmes si le projet déclare un packageManager dans son package.json. Il faut alors le désactiver explicitement.

Ce que la mémoire vive demande en échange

Mettre des fichiers en mémoire n’est pas gratuit en vigilance. La relecture de sécurité de notre outillage a relevé de vrais défauts, tous corrigés et couverts par un test :

  • le nettoyage jugeait l’âge d’un dossier sur sa propre date, qui ne bouge pas quand une tâche écrit plus profond : il pouvait supprimer le dossier d’une tâche encore active ;
  • des scripts lancés en administrateur écrivaient dans des dossiers modifiables par les tâches : une tâche malveillante aurait pu en profiter ;
  • le montage du tmpfs devait refuser un chemin qui passe par un lien symbolique, sinon la mémoire vive pouvait être montée ailleurs que prévu.

Chaque garde-fou est prouvé par un test qui échoue quand on le retire. Par exemple, sans le vidage du dossier temporaire, le test « chaque runner a son propre dossier, vidé à la tâche suivante » passe au rouge.

Autre règle, rappelée par GitHub dans sa documentation sur les runners auto-hébergés : un dépôt public ne doit pas exécuter les propositions de modification d’inconnus sur votre propre machine. Nos runners ne servent que des dépôts privés.

Faut-il le faire chez vous ?

Notre conclusion tient en trois lignes :

  1. Commencez par mesurer, étape par étape, avec l’API de votre CI. Sans relevé « avant », aucun gain ne se démontre.
  2. Cherchez d’abord les pertes bêtes : un cache inutile, un double déclenchement (nous avons vu une même suite tourner deux fois sur le même commit, 124 s au lieu de 73 s), un verrou trop large.
  3. La mémoire vive aide si vos tests écrivent beaucoup de façon synchrone et si le serveur est chargé. Sur un serveur calme, elle ne change rien.

Ce chantier fait partie de notre façon de travailler : automatiser, mesurer, publier les chiffres avec leurs limites. C’est la même méthode que nous appliquons aux automatisations que nous construisons pour nos clients et à la performance des sites ; nos études de cas en donnent d’autres exemples chiffrés. Pour en parler sur votre projet, écrivez-nous.