Mise en production
Déployez Potato en production avec gunicorn, nginx ou Apache, la terminaison TLS, Docker, la persistance des sessions et le contrôle d'accès qu'exige un serveur partagé.
Déployez Potato pour des tâches d'annotation réelles avec plusieurs utilisateurs, la persistance des données et une sécurité adaptée.
Configuration du serveur
Configuration de production de base
server:
host: 0.0.0.0
port: 8000
debug: falseLe bloc server accepte host, port et debug, et rien d'autre. Le nombre de workers et les délais d'expiration relèvent du serveur WSGI qui exécute Potato, pas du fichier de configuration. Laissez debug désactivé en production, car le mode debug ouvre les pages d'administration sans clé aux requêtes venant de la machine du serveur elle-même, ce qui inclut, sur un hôte déployé, son proxy inverse.
Utiliser HTTPS
La configuration de Potato ne contient aucun réglage TLS, donc les certificats ne s'y configurent pas. Terminez TLS au niveau du proxy inverse placé devant Potato, ce que mettent en place les sections nginx et Apache ci-dessous, ou confiez directement le certificat à gunicorn comme indiqué sous « Avec SSL ».
Exécution avec Gunicorn
Exécutez Potato sous gunicorn avec exactement un worker, et augmentez le nombre de threads si vous avez besoin de plus de concurrence :
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:8000 \
--timeout 120Le worker unique est une exigence de correction et non un choix de réglage. Potato garde en mémoire, dans le processus, son pool d'items, sa file d'attribution et l'état de chaque annotateur. Un second worker reçoit sa propre copie de ces trois éléments. Il distribue donc 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 efface les annotations de l'autre. Rien ne signale la perte, et les annotations disparaissent tout simplement. Les threads partagent une seule copie de cet état, si bien que --threads augmente la concurrence sans risque, ce que --workers ne permet pas.
Avec SSL
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:443 \
--certfile /path/to/cert.pem \
--keyfile /path/to/key.pemMise en place d'un proxy inverse
Configuration de Nginx
upstream potato {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name annotation.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
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;
location / {
proxy_pass http://potato;
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;
# Long annotation sessions
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
}Configuration d'Apache
<VirtualHost *:443>
ServerName annotation.example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
<Proxy *>
Order allow,deny
Allow from all
</Proxy>
</VirtualHost>Montage sous un chemin d'URL
Servir Potato à https://host/app1/ plutôt qu'à la racine d'un domaine demande une étape de plus. La page d'annotation demande /static/... et /updateinstance par rapport à la racine publique. Sous un préfixe de chemin, les ressources renvoient donc une erreur 404 et les annotations ne s'enregistrent pas, alors que l'interface s'affiche encore.
Indiquez à Potato son préfixe public. Si vous contrôlez le proxy et qu'il peut envoyer des en-têtes de transfert :
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000location /app1/ {
proxy_pass http://127.0.0.1:8000/; # trailing slash strips /app1/
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /app1;
}N'activez POTATO_PROXY_FIX que si l'application n'est accessible que par ce proxy. Cette option fait confiance aux en-têtes de transfert, donc un client capable d'atteindre directement le port interne pourrait falsifier le préfixe et corrompre toutes les URL générées.
Si vous ne pouvez pas ajouter d'en-têtes mais connaissez le chemin de montage, définissez-le directement. Le proxy doit tout de même retirer le préfixe avant de transférer la requête :
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000Testez en vous connectant depuis un navigateur sans session, car le formulaire de connexion est la première page que casse un préfixe manquant.
Les visionneuses d'agent en direct et de codage en direct diffusent leurs données par Server-Sent Events, qu'un proxy avec mise en tampon retient jusqu'à la fin du flux. Désactivez la mise en tampon sur ces routes :
location /app1/api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}Déploiement avec Docker
Potato publie une image prête à l'emploi sur GitHub Container Registry, ghcr.io/davidjurgens/potato. Elle contient Potato et ses dépendances, et rien d'autre. Votre dossier de projet, avec config.yaml et ses données, est monté sur /app, 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:latestLe tag latest et les tags de version, comme 2.12.1, ne contiennent que les dépendances de base. Les tags -all (latest-all, 2.12.1-all) ajoutent les SDK d'IA, les lecteurs de formats ainsi que les extras d'export et d'authentification, et laissent de côté les modèles de vision.
Le conteneur sert la tâche avec un seul worker gunicorn et huit threads, pour la raison donnée dans la section gunicorn ci-dessus. Définir GUNICORN_WORKERS au-delà de 1 arrête le conteneur au démarrage, sauf si POTATO_ALLOW_MULTIWORKER=1 est aussi défini, ce qui fait perdre des annotations dans une étude réelle. Augmentez GUNICORN_THREADS pour plus de concurrence. POTATO_CONFIG désigne un fichier de configuration autre que config.yaml, et PORT remplace le port 7860 à l'intérieur du conteneur.
Le conteneur s'exécute sous l'uid 1000 et écrit dans /app la sortie des annotations, potato.log et ses bases de données SQLite. Sous Linux, un dossier de projet monté par bind mount qui appartient à un autre utilisateur arrête le serveur au démarrage, avec une erreur qui nomme le problème. Attribuez le dossier à l'uid 1000 avec sudo chown -R 1000:1000 myproject. Docker Desktop sur Mac ignore le propriétaire des bind mounts, donc l'erreur n'apparaît que sous Linux.
Les arguments placés après le nom de l'image remplacent la commande du serveur, ce qui sert pour des vérifications ponctuelles :
docker run -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strictDocker Compose
services:
potato:
image: ghcr.io/davidjurgens/potato:latest
ports:
- "8000:7860"
volumes:
- ./myproject:/app
environment:
- POTATO_SECRET_KEY=${POTATO_SECRET_KEY}
- POTATO_ADMIN_API_KEY=${POTATO_ADMIN_API_KEY}
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./certs:/etc/ssl/certs
depends_on:
- potato
restart: unless-stoppedDans le réseau Compose, nginx atteint Potato par le nom du service sur le port du conteneur. Le bloc upstream de la section sur le proxy inverse ci-dessus devient donc server potato:7860; au lieu de server 127.0.0.1:8000;.
Comme le dossier de projet est monté et non copié dans une image, les annotations que Potato y écrit restent sur l'hôte quand le conteneur est remplacé.
Contrôle d'accès
Restreindre les inscriptions
Par défaut, Potato autorise l'auto-inscription. Sur un serveur public, listez les personnes autorisées à s'inscrire :
user_config:
allow_all_users: false
users:
- annotator1@example.com
- annotator2@example.comLes deux clés se placent sous user_config. Écrites au niveau supérieur, elles sont ignorées, et l'on croit avoir une liste d'accès alors qu'elle laisse entrer tout le monde.
Accès administrateur
admin_api_key est une clé partagée envoyée dans l'en-tête X-API-Key. Si vous ne la définissez pas, Potato en génère une et l'écrit dans {task_dir}/admin_api_key.txt. Sur un hôte de production, définissez-la donc vous-même et gardez-la hors du dépôt :
admin_api_key: "your-shared-key"
rbac:
enabled: true
user_role_assignments:
lead@example.com: adminAvec rbac configuré, les personnes nommées accèdent à /admin en se connectant normalement, sans faire circuler la clé partagée. Consultez Gestion des utilisateurs pour l'ensemble des permissions.
Sessions
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200secret_key signe le cookie de session et peut aussi venir de POTATO_SECRET_KEY. Sans clé, Potato en génère une nouvelle à chaque démarrage, et chaque redémarrage déconnecte donc tout le monde en pleine tâche. Sur un serveur qui redémarre à chaque déploiement, c'est le réglage qui coûte le plus souvent des annotations.
session_timeout_minutes efface une session restée inactive pendant cette durée, et le décompte repart à chaque requête. Potato 2.11 est la première version qui l'applique, avec une valeur par défaut de 480 minutes. À 60, comme dans cet exemple, un annotateur qui s'absente une heure doit se reconnecter à son retour.
Sortie des annotations
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300Potato écrit un répertoire par annotateur sous output_annotation_dir, chacun contenant le user_state.json de cette personne. export_annotation_format indique les formats écrits par l'export périodique, et une valeur vide signifie aucun export automatique. auto_export_interval est l'intervalle entre deux exports, en secondes, et vaut 60 par défaut.
Backend de base de données
Par défaut, les annotations sont stockées sur disque. Pour les déploiements qui ont besoin d'une base de données, database.type accepte mysql ou file :
database:
type: mysql
host: localhost
port: 3306
database: potato_db
username: potato
password: ${DB_PASSWORD}Les clés sont database et username, et non name et user. Une chaîne de connexion peut aussi venir de POTATO_DB_CONNECTION, ce qui garde l'identifiant hors du fichier de configuration.
Installez le pilote avec pip install 'potato-annotation[mysql]'. Avant la 2.10.2, le backend MySQL ne s'activait jamais, et une configuration qui le désignait stockait les annotations sur disque sans rien signaler. Depuis la 2.10.2, un pilote manquant ou un serveur injoignable bloque le démarrage, et l'export, les sauvegardes et potato deploy pull lisent la base de données.
Vérifications de santé et journalisation
Potato sert GET /health sur tout déploiement, et cette route ne demande aucune authentification. Elle peut donc servir de sonde de disponibilité pour un répartiteur de charge ou un orchestrateur :
curl -sf https://annotation.example.com/healthLe niveau de détail des journaux se règle dans la configuration ou en ligne de commande, et non dans un bloc de journalisation :
verbose: true
debug_log: serverdebug_log accepte all, ui, server ou none. La variable d'environnement POTATO_LOGGERS restreint la sortie à des loggers nommés quand vous cherchez un problème dans un sous-système précis.
Mise à l'échelle
Potato s'exécute comme un seul processus qui garde tout l'ensemble d'items en mémoire. Pour monter en charge, agrandissez donc la machine plutôt que de multiplier les serveurs. Faire tourner plusieurs réplicas derrière un répartiteur de charge ne répartit pas le travail. Chaque réplica garde sa propre copie de la file d'attribution et de l'état des annotateurs, ce qui reproduit exactement la perte de données décrite plus haut dans la section gunicorn.
La mémoire est la ressource qui croît avec le jeu de données. Autour d'un million d'items, les contenus peuvent passer dans un cache SQLite :
item_store:
backend: paged # default: memory
cache_size: 2048Mesurez avant de l'activer. La pagination économise environ 28 % sur les items de vision et 51 % sur le texte, loin d'un ordre de grandeur, et elle rend le chargement du corpus environ 2,5 fois plus lent. Le fichier est un cache reconstruit à partir des fichiers de données à chaque démarrage, donc le supprimer ne pose aucun problème.
Pour les grands projets, les leviers efficaces sont de borner ce qui est attribué à chaque annotateur avec per_annotator_quota, de répartir le corpus entre des cohortes avec batch_assignment, et d'ajouter des fichiers de données au fil du temps avec data_directory plutôt que de tout charger au démarrage.
Stratégie de sauvegarde
Une sauvegarde doit couvrir davantage que le répertoire de sortie des annotations. Le répertoire de la tâche contient aussi jusqu'à trois bases de données SQLite, dont aucune ne peut être reconstruite à partir des fichiers de données : project.sqlite (les mémos, le codebook, le workflow de révision), datasets.sqlite, et engine.sqlite quand le moteur d'étiquetage est utilisé. Potato stocke les comptes créés par le formulaire d'inscription dans user_config.json. Les bases de données fonctionnent en mode WAL pendant que le serveur y écrit, si bien qu'un tar ou un cp d'un fichier de base de données peut produire une copie corrompue ou privée du travail récent, et aucune des deux commandes ne vous en avertit.
La sauvegarde intégrée
Depuis la version 2.10, le bloc backup de Potato copie hors du serveur la sortie des annotations et des instantanés .backup des bases de données, et les restaure quand le serveur démarre avec un disque vide. La restauration ramène la liste des comptes avec les annotations, et elle n'écrase jamais des annotations déjà présentes sur le disque. Le bloc ci-dessous envoie la copie vers un bucket S3 toutes les cinq minutes, l'intervalle par défaut, si bien qu'un disque perdu coûte au plus cinq minutes de travail aux annotateurs :
backup:
schedule_minutes: 5
restore_on_boot: true
sinks:
- type: s3
bucket: lab-potato-backups
prefix: pilot-study
endpoint_url: https://s3.example.edu # leave out for AWS S3endpoint_url dirige la destination vers tout service qui parle l'API S3, comme un stockage objet universitaire, MinIO, Cloudflare R2 ou Backblaze B2. Les identifiants proviennent de POTATO_S3_ACCESS_KEY_ID et POTATO_S3_SECRET_ACCESS_KEY dans l'environnement du serveur, ou de la chaîne habituelle de boto3. Une clé limitée à ce seul bucket restreint ce à quoi le serveur peut accéder. Une destination type: huggingface avec un repo_id envoie plutôt la copie vers un dataset Hugging Face privé, en utilisant HF_TOKEN. Les deux destinations nécessitent l'extra hosting, pip install 'potato-annotation[hosting]'.
Une copie qui reste sur votre propre stockage
Quand un accord d'utilisation des données impose de garder les données sur le serveur, une tâche nocturne peut plutôt les copier vers un stockage local ou monté. Le script ci-dessous archive le répertoire de sortie, prend un instantané de chaque base de données avec la commande .backup de SQLite au lieu de copier le fichier, et conserve sept jours de copies. Il nécessite l'outil en ligne de commande sqlite3 :
#!/bin/bash
# backup.sh
set -e
TASK_DIR=/var/potato
DEST=/var/backups/potato/$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEST"
tar -czf "$DEST/annotations.tar.gz" -C "$TASK_DIR" annotations
for db in project.sqlite datasets.sqlite engine.sqlite; do
if [ -f "$TASK_DIR/$db" ]; then
sqlite3 "$TASK_DIR/$db" ".backup '$DEST/$db'"
fi
done
cp "$TASK_DIR/config.yaml" "$DEST/"
if [ -f "$TASK_DIR/user_config.json" ]; then cp "$TASK_DIR/user_config.json" "$DEST/"; fi
find /var/backups/potato -mindepth 1 -maxdepth 1 -mtime +7 -exec rm -rf {} +Exécutez-le depuis cron à 2 h chaque nuit :
0 2 * * * /var/potato/backup.shUne copie nocturne peut perdre jusqu'à une journée de travail, donc exécutez-la plus souvent pendant une étude qui compte beaucoup d'annotateurs. Avant le début de l'étude, restaurez une copie sur un serveur de test et vérifiez que la dernière annotation s'y trouve.
Liste de contrôle de sécurité
- Terminer TLS au niveau du proxy avec un certificat valide
- Définir
secret_key, ouPOTATO_SECRET_KEY, pour que les sessions survivent à un redémarrage - Définir
admin_api_keyplutôt que de livrer le fichier généré - Restreindre les inscriptions avec
user_config.users - Laisser
debugdésactivé, car le mode debug laisse les requêtes du proxy inverse atteindre les pages d'administration sans clé - Exécuter gunicorn avec un seul worker
- Garder les identifiants dans des variables d'environnement, pas dans la configuration
- N'activer
POTATO_PROXY_FIXque derrière un proxy de confiance - Sauvegarder le répertoire de sortie, le fichier des comptes et les bases de données, et tester une restauration
- Vérifier qui détient un rôle d'administrateur
Variables d'environnement
# .env file
POTATO_SECRET_KEY=your-super-secret-key
POTATO_ADMIN_API_KEY=your-admin-key
POTATO_DB_CONNECTION=mysql://potato:password@localhost:3306/potato_dbGénérez une clé de signature avec :
python3 -c 'import secrets; print(secrets.token_hex(32))'Chargez-les dans Docker Compose :
services:
potato:
env_file:
- .envDépannage
Consulter les journaux
# Gunicorn logs
journalctl -u potato -f
# Docker logs
docker-compose logs -f potatoProblèmes courants
502 Bad Gateway : vérifiez que Potato est lancé et écoute sur le port vers lequel le proxy transfère les requêtes.
Tout le monde est déconnecté après un déploiement : secret_key n'est pas défini, donc une nouvelle clé de signature a été générée au démarrage.
Annotations manquantes : vérifiez le nombre de workers. Plus d'un worker gunicorn efface des annotations sans le signaler.
Ressources en 404 sous un préfixe de chemin : définissez POTATO_PROXY_FIX ou POTATO_URL_PREFIX, et vérifiez que le proxy retire le préfixe avant de transférer la requête.
Lenteur avec un grand corpus : ajoutez de la mémoire avant d'ajouter des serveurs, et envisagez item_store.backend: paged au-delà d'environ un million d'items.
Étapes suivantes
- Gestion des utilisateurs - Listes d'accès, rôles et quotas
- Crowdsourcing - Prolific, SONA et d'autres panels
- Formats de données - Stockage et export
Pour les détails d'implémentation, consultez le guide du proxy inverse et le guide de mise à l'échelle.