Déployer Potato sur un serveur
Exécutez Potato en production derrière nginx avec TLS, dans Docker ou sur une VM, et comprenez pourquoi un serveur Potato tourne en un seul processus plutôt qu'en plusieurs workers.
Exécutez un processus Potato par projet, placez nginx et un certificat TLS devant lui, et augmentez le nombre de threads plutôt que de processus worker quand il vous faut plus de concurrence. Un second processus worker perd des annotations sans le signaler.
Potato garde en mémoire, dans le processus serveur, son pool d'éléments, sa file d'attribution et l'état de chaque annotateur. Ce seul fait détermine l'essentiel d'un déploiement. C'est à cause de cet état en processus qu'un serveur Potato monte en charge par les threads plutôt que par les workers, que le répertoire de sortie des annotations est ce qu'il faut sauvegarder, et que la répartition de charge entre plusieurs processus Potato corrompt une étude au lieu de l'accélérer. Ce guide couvre les trois façons d'exécuter un serveur, le proxy inverse et TLS placés devant, et les clés de configuration qui comptent une fois que de vrais annotateurs l'utilisent.
Pourquoi un serveur Potato tourne en un seul processus
Un second worker gunicorn reçoit sa propre copie du pool d'éléments, de la file d'attribution et de l'état des utilisateurs. Il distribue des instances que le premier worker a déjà attribuées et, comme user_state.json est réécrit en entier à chaque sauvegarde, le worker qui sauvegarde en dernier écrase les annotations de l'autre. Rien dans les logs ne le signale. Des annotations disparaissent, et le premier signe est en général un annotateur qui affirme avoir étiqueté un élément qui semble maintenant intact.
L'image Docker publiée de Potato impose cette règle. Elle exécute un seul worker gunicorn et refuse de démarrer avec davantage, et POTATO_ALLOW_MULTIWORKER=1 lève ce refus pour tout ce qui n'est pas une vraie étude. La concurrence vient plutôt des threads, qui partagent une seule copie de l'état. L'image en utilise huit par défaut via GUNICORN_THREADS, ce qui suffit pour les dizaines d'annotateurs simultanés d'une étude typique.
Cette contrainte encadre le reste de ce guide. Exécutez un processus par projet, donnez-lui assez de mémoire pour le jeu de données, et servez-vous du proxy inverse pour TLS et les fichiers statiques, pas pour répartir la charge.
Trois façons d'exécuter un serveur
| Méthode | Ce qu'elle vous apporte | Quand l'utiliser |
|---|---|---|
potato deploy | Provisionne l'hôte, TLS et l'image à partir de votre configuration | Une étude que vous voulez lancer aujourd'hui |
| Image Docker | Les mêmes octets à chaque fois, sans compilation de dépendances | Une machine que vous administrez déjà |
| VM avec systemd | Contrôle complet de l'hôte et de l'environnement Python | Un serveur institutionnel avec ses propres règles |
potato deploy pour aller vite
potato deploy provisionne un hôte, installe l'image et configure TLS à partir du fichier de configuration que vous avez déjà :
potato deploy up myproject/config.yaml --provider digitaloceanQuelques minutes plus tard, vous avez une URL HTTPS à envoyer aux annotateurs, et potato deploy pull rapatrie les annotations. Depuis la 2.10.0, les cibles sont local, potato share et treize hébergeurs cloud, parmi lesquels AWS, Jetstream2, Hetzner, Fly et Railway, et elles diffèrent selon que les données survivent ou non à un redémarrage. Les sept cibles VM réunissent disque persistant, flux de logs et SSH, et un hébergeur qui efface son disque nécessite --backup, qui copie les annotations vers un dataset HuggingFace ou un bucket S3. Exécuter Potato en local ou dans le cloud les compare. Commencez par local, qui exécute le même bundle et la même image sans compte ni facture, et vous indique ce qui serait déployé avant que cela ne coûte quoi que ce soit.
L'image Docker publiée
Potato publie une image précompilée sur GitHub Container Registry. Elle contient le serveur et ses dépendances, et votre projet est monté sur /app à l'exécution, si bien qu'une seule image sert pour toutes les tâches :
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestL'image existe parce que les dépendances de base de Potato comprennent numpy, pandas, scipy et scikit-learn. Les installer sur une petite VM est lent dans le meilleur des cas. Sur une machine de 1 GB, la compilation est tuée faute de mémoire, et sur 2 GB elle prend de cinq à dix minutes à chaque redéploiement. Télécharger l'image prend moins d'une minute.
Deux tags comptent. latest contient les dépendances de base et pèse environ 840 MB, et latest-all ajoute les SDK d'IA, les formats de documents, l'export et les connexions OAuth, pour environ 1,5 GB. Les deux sont construits pour linux/amd64 et linux/arm64. Épinglez une version plutôt que latest pour une étude qui durera des mois, car latest change sans vous prévenir.
Définissez POTATO_SECRET_KEY sur tout ce qui dure plus d'une seule exécution. Sans elle, la clé de signature des sessions Flask est aléatoire à chaque processus, donc chaque redémarrage déconnecte tous les annotateurs. POTATO_CONFIG choisit un chemin de configuration dans le conteneur, PORT fixe le port d'écoute (7860 par défaut) et GUNICORN_TIMEOUT fixe le délai d'expiration du worker en secondes.
L'image s'exécute sous l'uid 1000, et un bind mount conserve les droits de l'hôte, donc sous Linux le conteneur ne peut souvent pas écrire dans un répertoire que vous avez créé. Le serveur le vérifie au démarrage et refuse de s'exécuter plutôt que de planter en plein démarrage. Donnez le répertoire à l'utilisateur du conteneur avec sudo chown -R 1000:1000 myproject, ou exécutez-le sous votre propre identité avec docker run --user "$(id -u):$(id -g)", ce qui laisse aussi les annotations à votre nom. Docker Desktop sous macOS et Windows ignore les droits sur les bind mounts, donc une configuration qui fonctionne sur un Mac peut quand même échouer sur un serveur Linux.
Installation sur une VM
Sur une machine que vous administrez directement, installez Potato dans un environnement virtuel et confiez le processus à systemd.
sudo apt update
sudo apt install python3 python3-venv python3-pip nginx certbot python3-certbot-nginx
sudo mkdir -p /opt/potato/projects
cd /opt/potato
python3 -m venv venv
source venv/bin/activate
pip install potato-annotation gunicorn
potato --versionLancez l'application sous gunicorn avec un worker et plusieurs threads. La fabrique d'application se trouve dans potato.flask_server:create_app et prend le chemin de la configuration :
gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
--chdir /opt/potato/projects/my_project \
"potato.flask_server:create_app('config.yaml')"Créez /etc/systemd/system/potato.service pour que le serveur redémarre après un reboot ou un plantage :
[Unit]
Description=Potato Annotation Server
After=network.target
[Service]
Type=simple
User=potato
Group=potato
WorkingDirectory=/opt/potato/projects/my_project
Environment="PATH=/opt/potato/venv/bin"
EnvironmentFile=/opt/potato/.env
ExecStart=/opt/potato/venv/bin/gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
"potato.flask_server:create_app('config.yaml')"
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetActivez-le et démarrez-le :
sudo systemctl enable potato
sudo systemctl start potato
sudo systemctl status potatoLe EnvironmentFile contient les secrets qui ne doivent pas figurer dans la configuration :
# /opt/potato/.env
POTATO_SECRET_KEY=a-long-random-value
OPENAI_API_KEY=sk-...Générez la clé secrète une seule fois avec openssl rand -hex 32 et gardez-la pendant toute l'étude. La changer invalide toutes les sessions et déconnecte tous les annotateurs.
nginx et TLS
Potato écoute sur localhost et nginx termine TLS devant lui. La limite d'upload compte pour les projets d'images et d'audio, où la limite par défaut de 1 MB sur le corps des requêtes rejette des médias sans que l'annotateur voie l'échec.
server {
listen 443 ssl;
http2 on;
server_name annotation.example.com;
ssl_certificate /etc/letsencrypt/live/annotation.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/annotation.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Activez le site et obtenez un certificat :
sudo ln -s /etc/nginx/sites-available/potato /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d annotation.example.com
sudo systemctl enable certbot.timerFermez tout le reste au niveau du pare-feu, car le port de Potato ne doit plus être joignable directement une fois qu'un proxy est placé devant :
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enableServir plusieurs projets sous un même nom d'hôte
Les institutions exposent souvent plusieurs serveurs Potato derrière un seul point d'accès HTTPS public, en associant des chemins à des ports locaux. L'interface d'annotation charge ses ressources et enregistre via des URL relatives à la racine, donc un serveur monté sur /app1 doit connaître son préfixe, faute de quoi le CSS renvoie 404 et les sauvegardes automatiques échouent avec « annotations not saved ». Potato lit le préfixe dans le SCRIPT_NAME WSGI, et il y a deux façons de le définir.
Utilisez POTATO_PROXY_FIX=1 si vous contrôlez le proxy et qu'il peut envoyer X-Forwarded-Prefix. Utilisez POTATO_URL_PREFIX=/app1 si la configuration du proxy n'est pas de votre ressort. Le proxy doit toujours retirer le préfixe avant de transmettre, pour que Flask continue de recevoir des chemins sans préfixe :
location /app1/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /app1;
}ProxyFix fait confiance aux en-têtes transmis, donc n'activez POTATO_PROXY_FIX que si l'application n'est joignable qu'à travers le proxy de confiance. Si le port interne est aussi joignable directement, un client peut usurper le préfixe et corrompre les URL que Potato génère.
La configuration d'un projet en production
Quatre réglages distinguent une configuration de production d'une configuration locale : une clé de signature stable, l'exigence d'un mot de passe, une durée de session explicite et un répertoire de sortie que vous sauvegardez.
annotation_task_name: "Production Annotation Task"
task_description: "Classify each passage."
host: 127.0.0.1
port: 8000
# Sessions survive a restart only when the signing key is stable.
# Set it here, or leave it out and supply POTATO_SECRET_KEY in the environment.
require_password: true
persist_sessions: true
session_timeout_minutes: 480
data_files:
- data/items.jsonl
item_properties:
id_key: id
text_key: text
output_annotation_dir: annotation_output/production/
export_annotation_format: jsonl
user_config:
allow_all_users: false
users:
- annotator1
- annotator2
annotation_schemes:
- annotation_type: radio
name: sentiment
description: "What is the sentiment expressed in this text?"
labels:
- name: positive
- name: neutral
- name: negative
label_requirement:
required: truesession_timeout_minutes borne le temps d'inactivité et non la durée d'une journée de travail, car le compteur repart à chaque requête. La valeur par défaut de 480 convient à des annotateurs qui travaillent une journée entière ; réduisez-la quand la machine est partagée. Mettez allow_all_users: false et listez vos annotateurs dès que le serveur est joignable depuis l'internet public, pour qu'un inconnu qui trouve l'URL ne puisse pas s'inscrire et commencer à écrire dans votre jeu de données.
Contrôles de santé et logs
/health est une sonde de disponibilité non authentifiée pour les conteneurs et les répartiteurs de charge, et elle ne révèle presque rien, délibérément, car tout ce qu'elle dit est public :
curl http://localhost:8000/health
# {"status": "ok"} once the state managers exist
# {"status": "starting"} 503 while the server is still coming upLe healthcheck d'un conteneur et l'interrogation post-création d'un fournisseur de déploiement se fondent tous deux sur cette transition. Les noms de tâche, le nombre d'éléments et le nombre d'annotateurs viennent plutôt de /admin/health, qui exige la clé d'API d'administration.
Les logs passent par le processus, donc systemd les capture :
sudo journalctl -u potato -fAugmentez le niveau depuis la configuration avec verbose: true, ou activez les parties de la journalisation de débogage dont vous avez besoin avec debug_log, qui accepte all, ui, server ou none. Laissez les deux désactivés pendant une vraie étude, car la journalisation de débogage est assez bavarde pour remplir un petit disque au cours d'une longue exécution.
Quoi sauvegarder
Trois éléments du répertoire du projet ne peuvent pas être régénérés. La sortie des annotations sous output_annotation_dir est l'étude elle-même. project.sqlite contient les mémos, le livre de codes, les cas, l'index de recherche et le workflow de revue. datasets.sqlite contient les enregistrements du jeu de données. Tout le reste peut être reconstruit à partir de vos fichiers de données et de votre configuration.
#!/bin/bash
# /opt/potato/backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/opt/potato/backups
tar -czf $BACKUP_DIR/annotations_$DATE.tar.gz \
/opt/potato/projects/*/annotation_output/ \
/opt/potato/projects/*/project.sqlite \
/opt/potato/projects/*/datasets.sqlite \
/opt/potato/projects/*/config.yaml
find $BACKUP_DIR -mtime +7 -deleteExécutez-le chaque nuit depuis crontab, et copiez l'archive hors de la machine :
0 2 * * * /opt/potato/backup.shDimensionner la machine
La mémoire est la ressource qui croît avec le jeu de données, car le pool d'éléments est conservé dans le processus. Tout le reste est chargé à la demande, donc la pile d'embeddings et sentence-transformers n'est pas importée au démarrage sauf si une fonctionnalité en a besoin. Sur le jeu de données de référence des notes de version de la v2.6.0, la mémoire résidente est passée d'environ 750 MB à environ 365 MB, et le démarrage avec 50 000 éléments d'environ 10 secondes à environ 5,7 secondes.
Le coût par élément est faible et mesurable. Un élément de vision contenant une URL d'image et deux champs occupe environ 932 octets, et un élément de texte d'environ 540 caractères uniques environ 1430 octets. Un million d'éléments de texte représente donc environ 1,43 GB en mémoire résidente, et c'est le chiffre sur lequel budgéter.
Pour les corpus où cela compte, le contenu des éléments peut résider dans un fichier SQLite avec un petit cache en mémoire :
item_store:
backend: paged
cache_size: 2048Ajoutez ce bloc à la configuration ci-dessus. Lisez la mesure avant de l'activer. La pagination économise 28 % sur les éléments de vision et 51 % sur le texte, pas un ordre de grandeur, et en contrepartie le chargement du corpus prend environ 2,5 fois plus de temps. En dessous d'un million d'éléments, elle ne vous apporte qu'un serveur plus lent et aucun bénéfice.
Préférez une machine avec plus de RAM plutôt que de répartir un projet sur plusieurs serveurs, car la répartition reproduit la défaillance multiprocessus décrite au début de ce guide. Quand une seule machine ne peut vraiment pas contenir le corpus, découpez plutôt le travail. L'attribution par lots donne à chaque annotateur une tranche, per_annotator_quota plafonne ce qui est attribué à une même personne, et un data_directory avec surveillance du répertoire vous permet d'ajouter des fichiers pendant que le serveur tourne.
Dépannage
Le port est déjà utilisé. Trouvez le processus et arrêtez-le, plutôt que de lancer un second Potato sur un autre port avec le même répertoire de projet :
sudo lsof -i :8000Permission refusée sur le répertoire du projet. L'utilisateur du service doit en être propriétaire, et dans Docker cet utilisateur est l'uid 1000 :
sudo chown -R potato:potato /opt/potatoLes annotateurs sont déconnectés après chaque redémarrage. POTATO_SECRET_KEY ou la valeur de configuration secret_key est absente, donc la clé de signature est régénérée à chaque démarrage.
Les ressources renvoient 404 et les sauvegardes automatiques échouent derrière un proxy. Le serveur ne connaît pas son préfixe d'URL. Définissez POTATO_PROXY_FIX=1 ou POTATO_URL_PREFIX, et vérifiez que le proxy retire le préfixe avant de transmettre.
Pour aller plus loin
- Configuration de production et Proxy inverse sur ce site
- Running Potato in Docker pour la référence de l'image
- Scaling and Large Datasets pour les mesures derrière les conseils de dimensionnement
- Mener des études d'annotation sur Prolific une fois le serveur joignable