Aller au contenu

Installation

Prérequis

  • Ubuntu 24.04 ou plus récent, 26.04 sur le serveur du CHU. Le script d'installation utilise apt, systemd et dpkg ; aucune autre distribution n'est prise en charge.
  • Python 3, présent d'origine sur Ubuntu. Le script d'installation lui confie la lecture des valeurs déclarées dans les fichiers d'environnement, par le contexte.py que le disque de livraison apporte à côté de lui ; l'outil de sauvegarde en a besoin lui aussi.
  • Un GPU NVIDIA. Le script s'arrête si aucun GPU NVIDIA n'est détecté. La configuration de référence est une carte de 16 Go de VRAM, sur laquelle gemma4:12b et Whisper large-v3 cohabitent.
  • Un accès Internet pendant l'installation, même lent : dépôts Ubuntu, Docker et NVIDIA pour les paquets du système, puis le registre pour les images du backend et du frontend, et Hugging Face pour le cache du modèle d'embedding, qui pèse 553 Mo et se reconstruit seul. Avec un disque de livraison, ni les modèles ni les images des trois moteurs ne sont téléchargés : ils viennent du disque, et c'est ce qui rend l'installation possible sur un lien de 1 à 2 Mb/s.
  • Un jeton de lecture du registre. Les images du backend et du frontend sont des paquets privés : sans jeton, le tirage est refusé. Voir Le jeton du registre.
  • Un accès au dépôt Git, pour une installation depuis les sources seulement. Le dépôt est privé et la machine reçoit alors une clé de déploiement en lecture seule. Un serveur installé depuis un disque de livraison n'a besoin d'aucun des deux : le script d'installation est sur le disque, à côté de ce qu'il installe.
  • Une soixantaine de gigaoctets libres : les modèles occupent trente gigaoctets dans ~/.cause/, dont 4,4 pour le second modèle de transcription, et les images vingt-quatre de plus sous /var/lib/docker, dont seize et demi pour Whisper et Ollama et 6,9 pour Qwen3-ASR. Ce dernier chiffre est ce que les fichiers de l'image posent une fois dépliés, et non les 10,5 Go que docker images annonce, qui sont la somme de ses couches.
  • Un jeton deSEC pour le certificat HTTPS, voir HTTPS. Sans certificat, l'application n'est pas joignable.
  • Optionnel : une interface Wi-Fi capable du mode point d'accès, si vous voulez le hotspot intégré, et une autre voie d'accès à la machine que cette interface, câble ou seconde carte. Le point d'accès prend l'interface pour lui ; le script refuse de prendre celle qui porte votre session.

Pilote NVIDIA

Si aucun pilote n'est présent, le script installe nvidia-driver-550. Sur les cartes récentes de génération Blackwell, par exemple la RTX 5060 Ti, ce pilote fermé ne fonctionne pas : seul le module ouvert convient, installé en DKMS pour survivre aux mises à jour du noyau. Installez dans ce cas le pilote vous-même avant de lancer le script, en suivant Dépannage GPU.

Accès au dépôt

Cette étape ne concerne que les machines qui construisent les images depuis les sources. Sur un serveur livré, passez directement à Installer depuis un disque de livraison.

Générez une clé dédiée sur le serveur, sans phrase de passe, et déclarez-la comme clé de déploiement en lecture seule sur le dépôt GitHub.

ssh-keygen -t ed25519 -C "cause-deploy" -f ~/.ssh/cause_deploy
cat >> ~/.ssh/config << 'EOF'
Host github.com
    IdentityFile ~/.ssh/cause_deploy
    IdentitiesOnly yes
EOF
cat ~/.ssh/cause_deploy.pub
ssh -T git@github.com

Lancer l'installation

Sur un serveur client, branchez le disque de livraison et lancez le script qui est dessus :

sudo bash /media/<vous>/<disque>/livraison-cause-<version>/setup.sh

Il n'y a rien d'autre à récupérer : le disque porte le script, les modèles et la version à installer. Voir Installer depuis un disque de livraison.

Depuis les sources, pour une machine de développement ou pour le laboratoire de l'éditeur :

git clone git@github.com:zero-filtre/cause.git
cd cause
git fetch --tags
git checkout v<version>
sudo ./scripts/setup.sh

La version à installer est le dernier tag publié. Le script est idempotent, dans les deux cas : le relancer ne casse rien, mais il réécrit certains fichiers qu'il gère, listés dans Exploitation.

Le script enchaîne :

  1. Docker et le plugin Compose, installés s'ils manquent. Votre utilisateur est ajouté au groupe docker, ce qui impose un redémarrage.
  2. Pilote NVIDIA, voir ci-dessus. Un redémarrage est demandé si le pilote vient d'être posé. Dans les deux cas le script s'arrête et vous demande de le relancer après sudo reboot.
  3. NVIDIA Container Toolkit, puis un test docker run --gpus all avec nvidia-smi.
  4. Répertoires sous ~/.cause/, et écriture de CAUSE_HOME dans le fichier .env du dépôt, ou dans ~/.cause/.env quand le script est lancé depuis un disque. Seule cette ligne est réécrite : sur un serveur livré, le même fichier porte la version et le modèle.
  5. Matériel de livraison : lancé depuis un disque, le script reconnaît le sien et ne demande rien. Lancé depuis le dépôt, il cherche un disque externe portant un dossier livraison-cause-<version>, propose ce qu'il trouve, et accepte aussi un chemin saisi à la main ; laisser vide construit les images depuis les sources, ce qui est le mode de développement. Voir Installer depuis un disque de livraison.
  6. Variables backend : le fichier .env.backend est généré à partir de votre environnement, et votre fichier de réglages .env.backend.overrides est créé s'il n'existe pas, puis laissé intact, voir Régler l'application. Puis la remontée des évaluations : le script nomme votre installation avec le numéro de licence de la machine, lu dans ~/.cause/licence.lic, et ne vous demande ce nom que si la licence n'est pas encore émise. Il vous demande ensuite les deux clés du stockage de l'éditeur, la secrète en saisie masquée, et les pose dans le .env.backend qu'il génère, où il les reprend à chaque passage suivant. Votre fichier de réglages n'est pas touché : il ne doit porter que ce que vous y avez écrit. Un second passage ne vous demande rien et ne change rien. Sans personne devant la machine, rien n'est demandé et rien n'est écrit ; CAUSE_SANS_QUESTIONS=1 supprime ces questions pour une installation en série. Voir Configuration.
  7. Portainer sur https://127.0.0.1:9443, dont le compte se crée au premier accès.
  8. Certificat HTTPS si le fichier desec.ini est présent, voir HTTPS.
  9. Construction et démarrage des quatre images, ou, sur un serveur livré, chargement des moteurs depuis le disque et tirage du backend et du frontend depuis le registre. La première construction prend cinq à dix minutes. Le script attend ensuite que chaque service soit en bonne santé, dans l'ordre Whisper, Ollama, backend, frontend, puis que le modèle Ollama soit téléchargé, ce qui peut prendre jusqu'à une heure selon la connexion.
  10. Sources d'empreinte machine : le script vérifie que les trois fichiers d'identité sont lisibles dans le conteneur backend et le dit. Une source manquante n'est qu'un avertissement, deux sont un échec : en production le serveur refuserait de démarrer. Voir La licence machine.
  11. Hotspot Wi-Fi nommé Cause, sans mot de passe, avec des QR codes imprimables dans ~/.cause/qrcodes/print.html. Le script reconnaît le gestionnaire réseau de la machine, NetworkManager ou netplan avec systemd-networkd, et refuse de reprendre l'interface qui porte votre session SSH ou la route par défaut. Désactivez l'étape avec CAUSE_NO_HOTSPOT=1. Voir Hotspot Wi-Fi.

Installer depuis un disque de livraison

C'est la procédure normale pour un serveur client. Le disque est préparé par l'éditeur et porte ce que la connexion du client ne rattraperait pas : les modèles de langue et de transcription, les natifs d'inférence, et les images des trois moteurs, Whisper, Qwen3-ASR et Ollama. Leurs archives, celles qui voyagent sur le disque, font dix gigaoctets à elles trois, 3,0 Go pour Whisper, 3,4 pour Qwen3-ASR et 3,5 pour Ollama, soit onze à vingt-deux heures de tirage sur un lien de 1 à 2 Mb/s. Elles sont presque entièrement de l'amont : 7,5 Go de base CUDA, 5 Go d'amont Ollama, 3,4 Go de bibliothèques NVIDIA pour Qwen3-ASR, et quelques centaines de mégaoctets d'assemblage.

Les images du backend et du frontend, elles, sont tirées du registre à la version que le disque déclare. Elles changent à chaque version, et ne pèsent que la couche du jar et celle du bundle, soit une vingtaine de minutes sur le même lien. C'est ce partage qui permet de mettre à jour l'application sans retourner chez le client, voir Mettre à jour.

Le script d'installation est sur le disque, à la racine du dossier de livraison, avec le contexte.py dont il se sert pour lire les valeurs déclarées dans les fichiers d'environnement. Le serveur n'a donc ni dépôt à cloner ni clé de déploiement à recevoir, et ces deux fichiers sont tout le code source qui parte chez le client. Lancé de là, le script reconnaît son propre disque et ne demande rien. Il enchaîne alors :

  1. Il contrôle les fichiers du disque, par leur nom et par leur taille, à partir du fichier TAILLES.txt que le disque porte. C'est instantané, 0,08 seconde pour trente gigaoctets. Un fichier absent, ou dont la taille a changé, est nommé puis l'installation continue : c'est à vous de décider. Voir Vérifier un disque sans rien installer.
  2. Il met les modèles en place dans ~/.cause/, sans jamais approcher consultations/, patients/ ni aucun autre dossier métier. Le script sert donc aussi bien à installer qu'à mettre à jour une machine qui soigne déjà. Seul ce qui manque ou a changé est recopié : les dates de modification viennent du disque, qui les a préservées, et disent ce qui est déjà en place.
  3. Il charge les images des moteurs depuis le disque, une à deux minutes chacune, puis les étiquette à la version livrée. L'archive, elle, ne porte pas la version dans son nom : images/cause-whisper.tar, images/cause-qwen-asr.tar, images/cause-ollama.tar, les couches des moteurs ne changeant pas quand l'application change. Une image déjà présente n'est pas relue, et une montée de version qui retrouve les couches déjà chargées ne fait que poser l'étiquette neuve. Une archive absente est nommée et l'installation continue, comme pour le reste du disque.
  4. Il pose le compose du client en ~/.cause/docker-compose.yml, avec son fichier .env, auquel il ajoute le chemin de CAUSE_HOME. Sur une machine qui a aussi le dépôt, le docker-compose.yml du dépôt n'est pas touché : un git pull le réécrirait, et plus personne ne saurait lequel des deux fait foi.
  5. Il déclare COMPOSE_FILE dans votre profil, pour que les commandes docker compose des manuels visent le compose du client où que vous les lanciez. Voir Quel compose fait foi.
  6. Il se connecte au registre, puis tire le backend et le frontend au lieu de les construire, et démarre. Les trois moteurs ne sont cherchés dans aucun registre : leur service porte pull_policy: never, parce que la machine est leur seule source. Si l'image manque, le démarrage s'arrête en la nommant.

Le disque peut porter plusieurs modèles, celui qui tourne et des replis. Le modèle actif est la ligne OLLAMA_MODEL du fichier .env, et en changer ne demande rien d'autre que de modifier cette ligne puis docker compose -f ~/.cause/docker-compose.yml up -d.

Ce même .env porte deux lignes de version, et ce n'est pas un doublon : CAUSE_VERSION est celle du backend et du frontend, qui se tirent du registre, et CAUSE_MOTEURS celle des trois images que le disque a apportées. Les deux ne changent pas au même rythme.

Il porte enfin KAUZE_DOMAINE, le nom du site, par exemple chu.kauze.tech. Le domaine n'est pas écrit dans l'image du frontend, de sorte qu'une seule image serve tous les sites : c'est cette ligne qui dit vers quel nom nginx redirige, et c'est pour ce nom que le certificat est émis. En changer demande un docker compose up -d et un certificat réémis, voir HTTPS.

Le second moteur de transcription, livré à l'arrêt

Qwen3-ASR transcrit les actes dictés, Whisper restant sur les actes interactifs. Son image voyage sur le disque comme les deux autres et le script la charge, ses 4,4 Go de poids sont posés dans ~/.cause/qwen-asr/hub par la mise en place, mais le compose du client le déclare derrière le profil qwen : docker compose up -d ne le démarre pas. C'est la VRAM qui décide et elle se mesure sur place : les deux cartes de la machine du CHU montent déjà à 26,5 Go au pic sous charge, quand le moteur demande 5,2 Go avec sa fenêtre audio bornée à une minute.

Le réveiller ne demande donc aucun téléchargement, une seule commande :

docker compose --profile qwen up -d qwen-asr

Démarré, le moteur ne transcrit encore rien : c'est ASR_DICTATED_PROVIDER qui lui envoie la dictée, voir Configuration.

Deux réglages du fichier .env comptent : QWEN_MAX_SECONDS, la fenêtre audio, qui décide à elle seule si le moteur tient dans ce qu'il reste de carte, et QWEN_DEVICE, qui nomme la carte, par exemple cuda:1, plutôt que de laisser auto prendre la première. Pour le rendormir : docker compose stop qwen-asr.

La banque audio de mesure

Le disque porte aussi test-audio/, six cent quatre-vingt-cinq mégaoctets de voix de volontaires dont se sert la campagne de charge jouée sur la machine installée. Elles voyagent toujours avec le disque, le lien du client ne les rapatriant pas.

L'installation ne les pose pas : un serveur qui soigne n'a aucune raison de recevoir du matériel d'essai. Pour les installer dans ~/.cause/test-audio, il faut le demander :

CAUSE_AVEC_TEST_AUDIO=1 sudo bash /media/<vous>/<disque>/livraison-cause-<version>/setup.sh

La campagne de qualité, elle, n'a besoin de rien sur la machine : les consultations qu'elle fait rédiger partent dans le corps de la requête.

Le jeton du registre

Les images du backend et du frontend sont des paquets privés, le jar portant les prompts et tout le code du produit. Le serveur a donc besoin d'un jeton, qui n'a que le droit de lire les paquets.

Créez sur GitHub un jeton personnel portant le seul droit read:packages, puis posez-le sur le serveur, avant la première installation :

umask 077
echo CAUSE_GHCR_USER=<compte> > ~/.cause/.env.ghcr
echo CAUSE_GHCR_TOKEN=<jeton> >> ~/.cause/.env.ghcr

L'umask compte : le script retire les droits des autres comptes s'il en trouve, et le dit. Il ne les élargit jamais, un fichier déjà plus fermé reste tel quel. Sans ce fichier et sans connexion déjà en place, l'installation s'arrête avant le tirage en nommant le fichier attendu et le droit à donner au jeton.

Ce fichier ne part jamais sur le disque de livraison, qui passe d'un client à l'autre : la préparation du disque n'y dépose que ce qu'elle y écrit elle-même. Un serveur déjà connecté au registre par un sudo docker login ghcr.io n'a pas à garder ce fichier : le script le constate et passe.

Le script remplit deux trousseaux : celui de root, qui tire les images pendant l'installation, et celui de votre compte, qui les tirera à chaque mise à jour. Sans le second, le docker compose pull de la section « Mettre à jour » répond « unauthorized ».

Quand le jeton expire, le remplacer dans ce fichier suffit ; la mise à jour suivante s'y reconnecte toute seule. Si le registre refuse le jeton du fichier alors que le serveur y est déjà connecté, l'installation continue avec la connexion en place et le signale.

Vérifier un disque sans rien installer

cd /media/<vous>/<disque>/livraison-cause-<version>
cat VERSION
while read -r o c; do [ "$(stat -Lc %s "$c")" = "$o" ] || echo "ECART $c"; done < TAILLES.txt

VERSION nomme la version livrée, le modèle actif, ses replis, les deux images à tirer du registre et les trois que le disque apporte. TAILLES.txt déclare la taille attendue de chaque fichier, une ligne par fichier, dans le format de sha256sum : la taille, deux espaces, le chemin. Cela se lit à l'oeil autant que par la boucle ci-dessus, qui ne nomme que les écarts et ne demande rien d'autre que les outils de base du système. Le script d'installation et son contexte.py sont inventoriés comme le reste du disque.

Le disque ne porte aucune liste de condensés : les blobs d'Ollama et de Hugging Face portent leur condensé en guise de nom, et un doute sur l'un d'eux se lève avec le fichier seul :

sha256sum modeles/ollama/models/blobs/sha256-a8bdd07a...

Si le condensé calculé n'est pas le nom du fichier, ce blob est abîmé.

Déposer la licence

En production, le serveur ne démarre qu'avec une licence émise pour cette machine. Après la première installation, relevez l'empreinte affichée par l'écran d'accueil ou par curl -s http://localhost:8090/api/licence, transmettez-la à l'éditeur, déposez le fichier reçu en ~/.cause/licence.lic puis docker compose restart backend. La procédure complète, y compris après un remplacement de matériel, est dans La licence machine.

Premier compte administrateur

Au premier démarrage, si aucun compte ADMIN n'existe, le backend crée l'utilisateur admin avec un mot de passe aléatoire de douze caractères et l'écrit dans ses logs :

docker compose logs backend | grep 'Mot de passe'

Changez ce mot de passe dès la première connexion, depuis l'onglet Utilisateurs de l'administration. Il reste lisible dans le fichier de log du jour tant que celui-ci existe.

Si un utilisateur nommé admin existe déjà avec un autre rôle, aucun compte n'est créé et un avertissement apparaît dans les logs.

Mettre à jour

Sur un serveur livré, la mise à jour applicative est une ligne à changer dans ~/.cause/.env, puis deux commandes :

sed -i 's/^CAUSE_VERSION=.*/CAUSE_VERSION=<version>/' ~/.cause/.env
docker compose pull
docker compose up -d

Le tirage demande le jeton du registre : un « denied » à cette étape veut dire que le jeton manque, a expiré ou a perdu son droit, voir Le jeton du registre.

Seuls le backend et le frontend sont tirés, parce que le compose dit des moteurs qu'ils ne sont dans aucun registre. Et seules les couches qui ont changé descendent : les bases Java et nginx sont épinglées et déjà sur la machine, donc comptez la couche du jar et celle du bundle, une vingtaine de minutes sur un lien de 1 à 2 Mb/s.

Changer de moteur est une autre affaire : dix gigaoctets d'archives ne passent pas par cette connexion. Cela demande un nouveau disque, ou un portable branché sur le réseau de l'établissement, puis le script du disque, qui charge l'image et met CAUSE_MOTEURS à jour. Seule l'image qui a bougé voyage, et elle se compte en gigaoctets, pas en minutes.

Sur une machine installée depuis les sources, chaque version publiée porte un tag git, et la mise à jour consiste à se placer sur le tag voulu et à reconstruire :

git fetch --tags
git checkout v<version>
docker compose build
docker compose up -d

Les données de ~/.cause/ ne sont pas touchées, dans les deux cas. Les fichiers de consultation sont migrés à la lecture si leur format a évolué.

Les mises à jour des paquets du système sont une autre opération, et elles ne sont pas automatiques : voir Mises à jour du système.