Leçon 1 — Installer

Le serveur MLflow et son stockage

6 min de lecture

La première tentative de l'équipe de Valoria a été un mlflow ui lancé sur le portable d'une développeuse, avec un dossier mlruns/ local. Deux semaines plus tard, ses collègues n'avaient rien vu de ses évaluations, le dossier pesait 400 Mo de traces, et un git clean l'a effacé. Un outil de suivi n'a de valeur que s'il est partagé, persistant, et qu'on sait le remettre à zéro sans rien oublier.

Le mécanisme

MLflow sépare deux stockages :

  • le magasin de suivi (backend store) : expériences, runs, paramètres, métriques, prompts, traces, registre de modèles. C'est une base SQL.
  • le magasin d'artefacts : fichiers produits par les runs (modèles empaquetés, tableaux d'évaluation). Avec --serve-artifacts, le serveur sert de mandataire : les clients envoient et lisent les artefacts par HTTP, sans accès direct au volume ni identifiants de stockage.
flowchart LR
  C["Clients Python<br/>lab.py, tests, CI"] -- "HTTP :5050" --> S["mlflow server<br/>2 workers"]
  S -- "SQLAlchemy + psycopg 3" --> P[("PostgreSQL 16<br/>volume pgdata")]
  S -- "--artifacts-destination" --> A[("/mlartifacts<br/>volume artifacts")]
  U["Navigateur"] -- "interface" --> S

PostgreSQL ou SQLite ? SQLite suffit à un seul processus qui écrit : c'est ce qu'utilisent les tests (sqlite:///<tmp>/mlflow.db). Le serveur, lui, reçoit des écritures concurrentes : mlflow.genai.evaluate exécute les prédictions et les juges dans plusieurs fils, chacun exporte ses traces, et le serveur tourne avec deux workers. SQLite verrouille la base entière à chaque écriture ; sous cette charge, les erreurs database is locked apparaissent. PostgreSQL gère ces écritures concurrentes, et c'est la base que vous utiliserez en équipe ; autant développer dessus.

La mise en œuvre

L'image officielle ghcr.io/mlflow/mlflow:v3.17.1 ne contient aucun pilote PostgreSQL, et SQLAlchemy 2.1.4, qu'elle embarque, choisit psycopg (version 3) pour une URI postgresql://. Le Dockerfile du projet ajoute exactement ce pilote :

files/docker/Dockerfile
FROM ghcr.io/mlflow/mlflow:v3.17.1
RUN pip install --no-cache-dir "psycopg[binary]==3.2.10"
files/docker-compose.yml (extrait)
  mlflow:
    build: ./docker
    image: lab-mlflow-server:3.17.1
    depends_on:
      db:
        condition: service_healthy
    command: >
      mlflow server --host 0.0.0.0 --port 5000
      --backend-store-uri postgresql://mlflow:${POSTGRES_PASSWORD:-mlflow-lab}@db:5432/mlflow
      --artifacts-destination /mlartifacts --serve-artifacts --workers 2
    ports:
      - "127.0.0.1:${MLFLOW_PORT:-5050}:5000"

Le port n'est publié que sur 127.0.0.1 : le serveur n'a aucune authentification dans cette configuration, il ne doit pas être joignable depuis le réseau. Le port hôte est 5050 parce que 5000 est souvent pris (AirPlay sous macOS).

Le Makefile pilote la pile sous le nom de projet lab-mlflow, attend les contrôles de santé, puis vérifie l'arrêt :

files/Makefile (extrait)
COMPOSE := MLFLOW_PORT=$(MLFLOW_PORT) docker compose -p lab-$(SLUG)
up: ## Démarre MLflow + PostgreSQL et attend qu'il réponde
	@$(COMPOSE) up -d --build --wait
	@curl -sf $(MLFLOW_TRACKING_URI)/health >/dev/null && echo "MLflow prêt : $(MLFLOW_TRACKING_URI)"
down: ## Arrête la pile et supprime ses volumes (runs, traces, artefacts)
	@$(COMPOSE) down -v --remove-orphans
	@test -z "$$(docker ps -aq --filter label=com.docker.compose.project=lab-$(SLUG))" && \
	  test -z "$$(docker volume ls -q --filter label=com.docker.compose.project=lab-$(SLUG))" && \
	  echo "Vérifié : aucun conteneur ni volume lab-$(SLUG)."

Le Makefile ne fait pas include .env suivi d'export : make up MLFLOW_PORT=5051 doit l'emporter sur .env. Il lit le port dans .env (défaut 5050) et le transmet explicitement à docker compose et, sous forme de MLFLOW_TRACKING_URI, à lab.py ; ceux-ci lisent le reste de .env eux-mêmes. Vérifié : make up MLFLOW_PORT=5051 puis curl -s localhost:5051/version répond 3.17.1.

Résultat obtenu (2026-10-16)

$ make up
 Container lab-mlflow-db-1  Healthy
 Container lab-mlflow-mlflow-1  Healthy
MLflow prêt : http://localhost:5050
$ docker ps --filter label=com.docker.compose.project=lab-mlflow
lab-mlflow-mlflow-1  lab-mlflow-server:3.17.1  Up 15 minutes (healthy)  127.0.0.1:5050->5000/tcp
lab-mlflow-db-1  postgres:16-alpine  Up 15 minutes (healthy)  5432/tcp
$ curl -s localhost:5050/version
3.17.1
$ make down
 Volume lab-mlflow_pgdata  Removed
 Volume lab-mlflow_artifacts  Removed
 Network lab-mlflow_default  Removed
Vérifié : aucun conteneur ni volume lab-mlflow.

Ce qui casse en production

  • Pilote absent. Symptôme : le conteneur redémarre en boucle avec ModuleNotFoundError: No module named 'psycopg'. Diagnostic : docker compose -p lab-mlflow logs mlflow. Parade : le Dockerfile ci-dessus ; installer psycopg2 ne sert à rien, ce n'est pas le pilote que choisit SQLAlchemy 2.1.
  • Serveur exposé. Un ports: "5050:5000" publie le serveur sur toutes les interfaces, sans mot de passe : n'importe qui sur le réseau lit les traces, donc les questions des collaborateurs. Parade : 127.0.0.1: en local ; en équipe, un mandataire authentifiant devant le serveur, ou l'authentification intégrée de MLflow.
  • Artefacts hors du serveur. Sans --serve-artifacts, le client écrit les artefacts directement dans le chemin déclaré, qui n'existe que dans le conteneur : log_model échoue sur le poste. Parade : mode mandataire, ou stockage objet partagé (bucket) accessible des deux côtés.
  • Volume oublié. docker compose down sans -v garde 400 Mo de traces, et le prochain make up repart avec d'anciennes versions de prompts. Parade : make down fait down -v et vérifie qu'aucun volume ne reste.

La règle de décision

En local et en équipe : MLflow serveur, base PostgreSQL, artefacts servis par le serveur. SQLite pour les tests et les essais d'un seul processus. Ne publiez jamais le port d'un serveur sans authentification au-delà de 127.0.0.1. Surveillez la taille de la base : les traces y croissent avec chaque appel.

Pratique

Une collègue lance mlflow server avec --backend-store-uri sqlite:///mlflow.db et --workers 4, puis une évaluation parallèle. Une partie des traces manque et le journal du serveur montre des erreurs. Que changez-vous ?

make down a affiché « Vérifié : aucun conteneur ni volume lab-mlflow », mais docker system df montre encore 1 Go d'images. Est-ce un défaut du nettoyage ?

Exercice. Ajoutez à la cible up une vérification qui échoue si le serveur répond mais ne peut pas écrire en base (par exemple : créer une expérience de test puis la supprimer).

Voir la solution
	@$(PY) -c "import mlflow; mlflow.set_tracking_uri('$(MLFLOW_TRACKING_URI)'); \
	  c = mlflow.MlflowClient(); e = c.create_experiment('lab-mlflow-sonde'); c.delete_experiment(e)" \
	  && echo "Écriture en base vérifiée."

/health répond dès que le processus HTTP est prêt, même si la base refuse les écritures (droits, disque plein). Une écriture réelle teste la chaîne complète. Une expérience supprimée reste dans la corbeille de MLflow : le nom lab-mlflow-sonde ne peut être réutilisé que si l'on supprime aussi la corbeille (mlflow gc), d'où un nom unique par exécution en pratique (lab-mlflow-sonde-<horodatage>).

  • Je démarre et j'arrête la pile sous un nom de projet dédié, volumes compris.
  • Je sais pourquoi le serveur utilise PostgreSQL et les tests SQLite.
  • Je ne publie le port que sur 127.0.0.1.
  • Je vérifie après make down qu'aucun conteneur ni volume ne reste.
Suivant