Exécuter Potato dans Docker
Servez un projet Potato depuis l'image Docker publiée, gardez ses annotations sur votre disque, et définissez la clé d'administration et les variables d'environnement que lit le conteneur.
Cette page explique comment exécuter un projet d'annotation Potato dans un conteneur, du premier docker run jusqu'au passage d'une étude en cours à une nouvelle version. Elle s'adresse aux chercheurs et aux administrateurs de laboratoire qui servent une étude depuis leur propre machine ou leur propre serveur et veulent utiliser Potato sans installation de Python sur l'hôte. Potato publie l'image dans GitHub Container Registry sous le nom ghcr.io/davidjurgens/potato, et l'image contient le serveur et ses dépendances, rien de plus. Comme votre dossier de projet est monté sur /app au démarrage du conteneur, une seule image sert toutes les tâches.
potato deploy exécute la même image sur les hébergeurs cloud que la commande met en place, et choisit le tag qui correspond à la version de Potato installée. Exécuter Potato en local ou dans le cloud couvre cette voie, et Mise en production couvre gunicorn, un proxy inverse et TLS sur un serveur que vous gérez.
Récupérer l'image
L'installation des dépendances numpy, pandas, scipy et scikit-learn de Potato avec pip manque de mémoire sur une machine de 1 Go et prend de cinq à dix minutes sur une machine de 2 Go. L'image contient déjà ces dépendances :
docker pull ghcr.io/davidjurgens/potato:2.14.0L'image existe en deux variantes, chacune avec un tag mobile, un tag par version et un tag par commit :
| Tag | Contenu |
|---|---|
latest, 2.14.0 | Les dépendances de base de Potato, les clients de sauvegarde (huggingface_hub et boto3) et Authlib pour les connexions SSO |
latest-all, 2.14.0-all | Tout ce qui précède, plus l'extra all avec les SDK d'IA derrière ai_support, l'ingestion de PDF et de DOCX, l'export parquet et le pilote MySQL |
sha-<short>, sha-<short>-all | Un commit de la branche master |
Les deux variantes sont construites pour linux/amd64 et linux/arm64, et la version arm64 s'exécute nativement sur Apple Silicon. Aucune des deux variantes n'inclut l'extra vision et ses wheels torch de plusieurs gigaoctets. La segmentation dans le navigateur fonctionne sans cet extra, car ONNX Runtime Web est livré dans le paquet Potato.
Épinglez un tag de version pour toute étude qui durera plus d'une journée. Le tag latest est reconstruit à partir de la branche master à chaque modification, et deux commandes docker pull à une semaine d'intervalle peuvent récupérer un code différent. Un tag de version comme 2.14.0 est publié une seule fois au moment de la version et n'est pas reconstruit ensuite.
Construire l'image vous-même
Construisez l'image depuis un clone du dépôt lorsque vous avez besoin d'un extra qu'aucune des deux variantes publiées n'inclut. L'argument de construction POTATO_EXTRAS accepte n'importe quel extra nommé dans le setup.py de Potato, ou plusieurs séparés par des virgules :
git clone https://github.com/davidjurgens/potato
cd potato
docker build -t potato .
docker build --build-arg POTATO_EXTRAS=all -t potato:all .
docker build --build-arg POTATO_EXTRAS=all,saml -t potato:all-saml .L'extra saml, pour la connexion SAML, ne fait pas partie de all. Le Dockerfile installe la bibliothèque libxmlsec1, contre laquelle l'extra se compile, uniquement quand POTATO_EXTRAS nomme saml. Exécutez une image construite localement par son tag, comme potato:all, partout où cette page nomme ghcr.io/davidjurgens/potato:2.14.0.
Exécuter un projet
Un dossier de projet contient config.yaml et chaque fichier que la configuration nomme. Les exemples de cette page utilisent une tâche de sentiment à trois items avec l'organisation suivante :
myproject/
├── config.yaml
└── data/
└── items.jsonlLa configuration garde chaque chemin relatif au dossier du projet :
annotation_task_name: "Sentiment pilot"
task_dir: .
data_files:
- data/items.jsonl
item_properties:
id_key: id
text_key: text
output_annotation_dir: annotation_output/
user_config:
allow_all_users: true
annotation_schemes:
- annotation_type: radio
name: sentiment
description: "What is the sentiment of this sentence?"
labels: [positive, neutral, negative]Chaque ligne de data/items.jsonl est un item :
{"id": "1", "text": "The battery lasts all day."}
{"id": "2", "text": "The screen cracked within a week."}
{"id": "3", "text": "It arrived on Tuesday."}Démarrez le serveur depuis le dossier qui contient myproject :
docker run -d --name potato -p 8000:7860 \
-v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:2.14.0La tâche est alors accessible à http://localhost:8000. L'argument -p 8000:7860 associe le port 8000 de votre machine au port 7860, sur lequel le serveur écoute à l'intérieur du conteneur. L'argument -v monte le dossier du projet sur /app. Le côté hôte doit être un chemin comme $PWD/myproject, car Docker lit un nom seul comme myproject comme le nom d'un nouveau volume vide. docker logs -f potato suit le journal du serveur, et docker stop potato arrête le serveur.
Un projet qui contient plusieurs configurations en choisit une avec POTATO_CONFIG, un chemin relatif à /app :
docker run -d --name potato -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_CONFIG=configs/pilot.yaml \
ghcr.io/davidjurgens/potato:2.14.0Exécuter une étude qui dure des semaines
Une étude qui dure des semaines a besoin d'une clé de signature de session qui reste la même d'un redémarrage et d'une mise à jour à l'autre. Sans POTATO_SECRET_KEY, chaque démarrage du serveur choisit une nouvelle clé aléatoire et déconnecte tous les annotateurs. Créez une fois un fichier d'environnement, en dehors du dossier du projet, et passez le même fichier à chaque docker run :
printf 'POTATO_SECRET_KEY=%s\nPOTATO_ADMIN_API_KEY=%s\n' \
"$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > potato.env
chmod 600 potato.env
docker run -d --name potato -p 8000:7860 \
-v "$PWD/myproject:/app" \
--env-file potato.env \
--restart unless-stopped \
ghcr.io/davidjurgens/potato:2.14.0La section sur la clé d'administration explique la seconde ligne du fichier. --restart unless-stopped relance le conteneur après un plantage ou un redémarrage de l'hôte.
Autres commandes de l'image
Tout ce qui suit le nom de l'image remplace le serveur. Vérifiez une configuration avec le validateur que fournit l'image, ou ouvrez un shell dans le conteneur :
docker run --rm -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:2.14.0 potato validate config.yaml --strict
docker run --rm -it -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:2.14.0 shPropriété des fichiers sous Linux
Le serveur de l'image s'exécute sous l'uid 1000, et un montage bind conserve la propriété que le dossier a sur l'hôte. Sous Linux, un dossier que vous avez créé appartient à votre propre uid, et un dossier créé par root appartient à root. Si le propriétaire du dossier n'est pas l'uid 1000, le serveur ne peut pas y écrire d'annotations. Le conteneur vérifie le dossier avant de démarrer le serveur et s'arrête avec ERROR: /app is not writable by uid 1000 plutôt que d'échouer au milieu du démarrage. L'une ou l'autre des commandes ci-dessous résout le problème :
sudo chown -R 1000:1000 myproject # hand the folder to the container user
docker run --user "$(id -u):$(id -g)" ... # or run the container as yourselfAvec --user, les annotations et le fichier de clé d'administration que Potato écrit appartiennent aussi à votre propre compte. Docker Desktop sur macOS et Windows ignore la propriété sur les montages bind, et un projet qui fonctionne sur un Mac peut quand même échouer sur un serveur Linux. POTATO_ALLOW_READONLY_APP=1 ignore la vérification. Potato écrit toujours les annotations, potato.log et ses bases SQLite dans le projet, donc ne définissez la variable que si ces chemins sont accessibles en écriture par des montages séparés.
Certains hôtes démarrent chaque conteneur en tant que root, quoi que l'image précise. Quand le conteneur démarre en root, il donne /app à l'utilisateur potato de l'image et démarre le serveur sous cet utilisateur. Définissez POTATO_RUN_AS_ROOT=1 pour conserver root à la place.
Conserver les annotations
Potato écrit tout ce qu'une étude recueille dans le dossier du projet. Les annotations vont sous output_annotation_dir, dans un répertoire par annotateur qui contient le user_state.json de cet annotateur. Deux bases SQLite se trouvent à côté de config.yaml. project.sqlite contient les mémos, le livre de codes, les cas, l'index de recherche et le flux de relecture, et datasets.sqlite contient les jeux de données d'évaluation et leurs versions. Aucune des deux bases ne peut être reconstruite à partir des fichiers de données. Avec un montage bind, tous ces fichiers sont sur votre disque et y restent quand le conteneur est supprimé.
output_annotation_dir doit se résoudre à l'intérieur du dossier du projet. Un chemin comme /data/annotations arrête le serveur au démarrage avec le message output_annotation_dir: Path '/data/annotations' resolves to '/data/annotations' which is outside the project directory '/app'. Gardez un chemin relatif comme annotation_output/. Le volume /data de l'image se trouve aussi en dehors du projet, et Potato n'y écrit rien.
Copiez une base hors d'un conteneur en cours d'exécution avec la commande de sauvegarde de SQLite, jamais comme un simple fichier. project.sqlite fonctionne en mode WAL pendant que le serveur écrit dans la base, et une copie simple peut être corrompue ou manquer du travail récent sans aucun avertissement. L'image inclut l'outil sqlite3 pour l'instantané :
docker exec potato sqlite3 /app/project.sqlite ".backup /tmp/snap.sqlite"
docker cp potato:/tmp/snap.sqlite ./project.sqliteLa plupart des hébergeurs de conteneurs gérés effacent le système de fichiers du conteneur quand celui-ci redémarre. Sur un tel hébergeur, gardez le projet sur un stockage persistant monté sur /app, ou ajoutez un bloc backup à la configuration. Le bloc copie les annotations et les instantanés des bases vers un bucket S3 ou un jeu de données Hugging Face selon un calendrier, et les restaure quand le serveur démarre avec un disque vide. Chaque variante de l'image inclut les clients dont le bloc a besoin.
Variables d'environnement
Le conteneur lit ces variables au démarrage :
| Variable | Valeur par défaut | Rôle |
|---|---|---|
POTATO_CONFIG | config.yaml | Chemin de la configuration dans le conteneur, relatif à /app |
PORT | 7860 | Port sur lequel le serveur écoute dans le conteneur |
GUNICORN_WORKERS | 1 | Processus workers ; toute autre valeur arrête le conteneur au démarrage |
GUNICORN_THREADS | 8 | Threads du worker unique, et nombre de requêtes servies en même temps |
GUNICORN_TIMEOUT | 120 | Secondes pendant lesquelles un worker peut rester silencieux avant que gunicorn le redémarre |
POTATO_SECRET_KEY | aléatoire à chaque démarrage | Signe les cookies de session ; secret_key dans la configuration est prioritaire |
POTATO_ADMIN_API_KEY | fichier généré | La clé d'API d'administration ; admin_api_key dans la configuration est prioritaire |
POTATO_GENERATED_TEMPLATES_DIR | choisi au démarrage | Où Potato écrit les gabarits de tâche qu'il construit au démarrage |
POTATO_BUNDLE_URL | non définie | URL d'une archive .tar.gz du projet à télécharger dans /app au démarrage |
POTATO_BUNDLE_SHA256 | non définie | Somme de contrôle que le projet téléchargé doit respecter |
POTATO_BUNDLE_TOKEN | non définie | Envoyée comme Authorization: Bearer avec le téléchargement |
POTATO_RUN_AS_ROOT | non définie | 1 garde le serveur sous root quand le conteneur démarre en root |
POTATO_ALLOW_MULTIWORKER | non définie | 1 permet à GUNICORN_WORKERS de dépasser 1, ce qui perd des annotations dans une vraie étude |
POTATO_ALLOW_READONLY_APP | non définie | 1 ignore la vérification au démarrage que /app est accessible en écriture |
Un conteneur qui change PORT a besoin du même port à droite de -p, comme dans -p 8000:9000 pour PORT=9000.
Nombre de workers et de threads
Le conteneur refuse de démarrer avec plus d'un worker gunicorn. Potato garde en mémoire, dans un seul processus, son pool d'items, sa file d'attribution et l'état de chaque annotateur. Un second worker détiendrait sa propre copie de ces trois éléments et attribuerait des items que le premier worker a déjà attribués. Chaque sauvegarde réécrit user_state.json en entier, et le worker qui sauvegarde en dernier effacerait les annotations de l'autre sans aucune erreur. Augmentez plutôt GUNICORN_THREADS pour servir plus de requêtes en même temps, car les threads partagent une seule copie de l'état.
Gabarits générés
Potato construit au démarrage un gabarit pour la page d'annotation de chaque tâche. En dehors d'un conteneur, le gabarit va dans le paquet installé dès que le répertoire du paquet est accessible en écriture. L'image installe Potato en root et exécute le serveur sous l'uid 1000, et le serveur ne peut donc pas écrire dans le répertoire du paquet. Potato écrit alors les gabarits dans un répertoire sous /tmp que seul l'utilisateur du serveur peut lire ou écrire, et le journal de démarrage nomme ce répertoire. Potato reconstruit les gabarits à partir de la configuration à chaque démarrage, et perdre le répertoire ne coûte rien. Définissez POTATO_GENERATED_TEMPLATES_DIR pour placer les gabarits ailleurs, par exemple sur un montage tmpfs.
Télécharger le projet au démarrage
Les hébergeurs qui ne font qu'exécuter une image, comme Render, Fly, Railway et ECS, n'ont aucun dossier à monter. Avec POTATO_BUNDLE_URL définie, le conteneur télécharge une archive .tar.gz du projet, compare le téléchargement à POTATO_BUNDLE_SHA256 et décompresse le projet dans /app avant le démarrage du serveur. Le conteneur écrit la somme de contrôle dans /app/.potato-bundle-sha, et un redémarrage sur un disque persistant ignore un téléchargement dont la somme de contrôle correspond. Dans un bundle construit par potato deploy, une nouvelle version du projet ne remplace jamais la sortie des annotations ni les bases SQLite déjà présentes sur le disque. potato deploy up définit ces variables pour vous.
La clé d'administration dans un conteneur
Le tableau de bord d'administration à /admin et l'API d'administration utilisent une seule clé partagée, envoyée dans l'en-tête X-API-Key ou saisie dans la page de connexion d'administration. Potato lit la clé d'abord dans la clé de configuration de premier niveau admin_api_key, puis dans la variable d'environnement POTATO_ADMIN_API_KEY. Dans un conteneur, la variable d'environnement garde la clé hors de config.yaml et hors de tout dépôt qui contient le projet. Le fichier potato.env de la section sur les études longues définit la variable, et la clé d'administration est alors ce que contient ce fichier :
grep POTATO_ADMIN_API_KEY potato.envQuand ni la configuration ni l'environnement ne définissent de clé, Potato génère une clé la première fois que quelqu'un ouvre /admin ou appelle un point d'accès d'administration. Potato écrit la clé générée dans admin_api_key.txt dans le répertoire de la tâche. Dans le conteneur, le répertoire de la tâche est /app, le dossier de projet monté. Le fichier n'est lisible que par l'utilisateur du serveur, et Potato relit le même fichier après un redémarrage. Le journal du serveur nomme le fichier et n'affiche jamais la clé. Lisez la clé à travers le conteneur :
docker exec potato cat /app/admin_api_key.txtSur un hôte Linux où vous avez donné le dossier du projet à l'uid 1000, votre propre compte ne peut pas lire admin_api_key.txt sans sudo. Un conteneur démarré avec --user "$(id -u):$(id -g)" écrit au contraire le fichier sous votre propre compte. Consultez la page Tableau de bord d'administration pour savoir ce que la clé ouvre.
Mode debug dans un conteneur
Gardez debug: false dans tout conteneur qui sert une étude. Avec debug: true dans la configuration, Potato change le comportement de deux fonctionnalités :
- Pages d'administration sans clé. Le tableau de bord d'administration et l'API d'administration se passent de la clé pour une requête provenant d'une adresse de bouclage qui ne porte ni
X-Forwarded-For, niX-Real-IP, niForwarded. Sur le réseau bridge par défaut de Docker, une requête vers le port publié atteint le conteneur depuis l'adresse du bridge et a toujours besoin de la clé. Avec--network host, une requête de la machine hôte est une requête de bouclage et n'a pas besoin de clé. Un proxy inverse placé dans le même espace de noms réseau et qui n'ajoute aucun en-tête de transmission fait paraître locale chaque requête qu'il relaie. - Compte debug partagé. Quand
debug_phaseest aussi définie, chaque visiteur est connecté sous un seul compte partagé,debug_user, et envoyé directement à cette phase. Le compte n'est pas administrateur, mais tous les visiteurs partagent les réponses du compte.
Le serveur journalise un avertissement au démarrage chaque fois que debug est activé. Une commande exécutée dans le conteneur avec docker exec est une requête de bouclage. En mode debug, docker exec potato curl -s http://127.0.0.1:7860/admin/health renvoie donc le rapport de santé de l'administration sans clé.
Vérifications de santé
GET /health ne demande aucune authentification et indique seulement si le serveur est en marche. La route renvoie {"status": "ok"} une fois le serveur prêt et le statut 503 tant que le serveur charge encore les données, et un répartiteur de charge ou un orchestrateur peut interroger la route comme sa vérification de disponibilité. Le HEALTHCHECK de l'image interroge /health toutes les 30 secondes et accorde une période de démarrage de 90 secondes pour les grandes tâches, et docker ps affiche le résultat. GET /admin/health indique le nom de la tâche et l'état des gestionnaires d'items et d'annotateurs, et la route exige la clé d'administration dans l'en-tête X-API-Key.
Docker Compose
Le dépôt de Potato ne fournit aucun fichier Compose. Mise en production montre un fichier Compose qui exécute cette image derrière nginx, avec le dossier du projet monté sur /app et les clés de session et d'administration lues depuis l'environnement.
Passer à une nouvelle version
Mettre Potato à jour remplace le conteneur et laisse en place le dossier du projet, avec toutes les annotations et toutes les bases de données. Avant de changer, récupérez la nouvelle version, vérifiez la configuration avec le validateur de la nouvelle version et faites un instantané de la base de données du projet. Remplacez 2.14.0 par la version vers laquelle vous passez :
docker pull ghcr.io/davidjurgens/potato:2.14.0
docker run --rm -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:2.14.0 potato validate config.yaml --strict
docker exec potato sqlite3 /app/project.sqlite ".backup /tmp/snap.sqlite"
docker cp potato:/tmp/snap.sqlite ./project-before-update.sqliteRemplacez ensuite le conteneur en cours d'exécution par un conteneur sur le nouveau tag, avec le même montage et le même fichier d'environnement :
docker stop potato
docker rm potato
docker run -d --name potato -p 8000:7860 \
-v "$PWD/myproject:/app" \
--env-file potato.env \
--restart unless-stopped \
ghcr.io/davidjurgens/potato:2.14.0Comme potato.env conserve la même POTATO_SECRET_KEY, les annotateurs restent connectés pendant le changement, et la clé d'administration ne change pas.
Dépannage
ERROR: config file not found: config.yaml. Le projet n'est pas monté là où le serveur le cherchait. L'erreur liste le contenu de /app pour que vous le compariez à votre argument -v. Une liste vide indique en général un nom seul comme -v myproject:/app. Docker lit un nom seul comme un nouveau volume vide et non comme votre dossier.
ERROR: /app is not writable by uid 1000. Le serveur ne peut pas écrire dans le dossier du projet. Donnez le dossier à l'uid 1000 ou exécutez le conteneur avec --user. Propriété des fichiers sous Linux donne les deux commandes.
output_annotation_dir ... is outside the project directory '/app'. La configuration place la sortie des annotations en dehors du dossier monté. Utilisez un chemin relatif comme annotation_output/.
Tout le monde est déconnecté après un redémarrage. Aucune clé de signature n'est définie, et le serveur a choisi une nouvelle clé aléatoire au démarrage. Définissez POTATO_SECRET_KEY dans un fichier d'environnement et passez le fichier à chaque docker run.
Le conteneur s'arrête avec une erreur qui nomme GUNICORN_WORKERS. Le nombre de workers est supérieur à 1. Définissez GUNICORN_WORKERS=1, ou supprimez la variable, et augmentez GUNICORN_THREADS à la place.
La page de connexion d'administration rejette toutes les clés. Le admin_api_key de la configuration est prioritaire sur POTATO_ADMIN_API_KEY, et les deux sont prioritaires sur admin_api_key.txt. Vérifiez d'abord la configuration, puis docker exec potato printenv POTATO_ADMIN_API_KEY.
Pour aller plus loin
- Exécuter Potato en local ou dans le cloud - les cibles
potato deployqui exécutent cette image pour vous - Mise en production - gunicorn, nginx, TLS, sessions et sauvegardes sur votre propre serveur
- Proxy inverse - servir Potato sous un préfixe de chemin d'URL
- Tableau de bord d'administration - ce que la clé ouvre
Pour les détails d'implémentation, consultez la documentation source.