Despliegue de Potato en un servidor
Ejecute Potato en producción detrás de nginx con TLS, en Docker o en una VM, y entienda por qué un servidor de Potato funciona como un solo proceso y no como varios workers.
Ejecute un proceso de Potato por proyecto, ponga nginx y un certificado TLS delante y, cuando necesite más concurrencia, aumente los threads en lugar de los procesos worker. Un segundo proceso worker pierde anotaciones sin avisar.
Potato mantiene en memoria, dentro del proceso del servidor, su pool de ítems, su cola de asignación y el estado de cada anotador. Ese único hecho decide la mayor parte de un despliegue. El estado en proceso explica por qué un servidor de Potato escala con threads y no con workers, por qué el directorio de salida de anotaciones es lo que hay que respaldar y por qué el balanceo de carga entre varios procesos de Potato corrompe un estudio en lugar de acelerarlo. Esta guía cubre las tres formas de ejecutar un servidor, el proxy inverso y TLS que van delante y las claves de configuración que importan cuando ya hay anotadores reales trabajando.
Por qué un servidor de Potato funciona como un solo proceso
Un segundo worker de gunicorn recibe su propia copia del pool de ítems, de la cola de asignación y del estado de los usuarios. Reparte instancias que el primer worker ya asignó y, como user_state.json se reescribe completo en cada guardado, el worker que guarda último descarta las anotaciones del otro. Nada en los logs lo informa. Las anotaciones desaparecen, y la primera señal suele ser un anotador que dice haber etiquetado un ítem que ahora parece intacto.
La imagen Docker publicada de Potato impone esta regla. Ejecuta un solo worker de gunicorn y se niega a arrancar con más, y POTATO_ALLOW_MULTIWORKER=1 anula esa negativa para cualquier cosa que no sea un estudio real. La concurrencia viene en cambio de los threads, que comparten una sola copia del estado. La imagen usa ocho por defecto mediante GUNICORN_THREADS, lo que atiende a las decenas de anotadores simultáneos de un estudio típico.
Esa restricción delimita el resto de esta guía. Ejecute un proceso por proyecto, dele memoria suficiente para el dataset y use el proxy inverso para TLS y archivos estáticos, no para repartir carga.
Tres formas de ejecutar un servidor
| Método | Qué le da | Cuándo usarlo |
|---|---|---|
potato deploy | Aprovisiona el host, TLS y la imagen a partir de su configuración | Un estudio que quiere en marcha hoy |
| Imagen Docker | Los mismos bytes cada vez, sin compilar dependencias | Una máquina que ya administra |
| VM más systemd | Control total del host y del entorno de Python | Un servidor institucional con sus propias reglas |
potato deploy para la vía rápida
potato deploy aprovisiona un host, instala la imagen y configura TLS a partir del archivo de configuración que ya tiene:
potato deploy up myproject/config.yaml --provider digitaloceanUnos minutos después tiene una URL HTTPS para enviar a los anotadores, y potato deploy pull descarga las anotaciones. Desde 2.10.0, los destinos son local, potato share y trece hosts en la nube, entre ellos AWS, Jetstream2, Hetzner, Fly y Railway, y se diferencian en si los datos sobreviven a un reinicio. Los siete destinos de VM reúnen disco persistente, streaming de logs y SSH, y un host que borra su disco necesita --backup, que copia las anotaciones a un dataset de HuggingFace o a un bucket de S3. Ejecutar Potato en local o en la nube los compara. Empiece con local, que ejecuta el mismo paquete y la misma imagen sin cuenta y sin factura, y le dice qué se desplegaría antes de que cueste nada.
La imagen Docker publicada
Potato publica una imagen precompilada en GitHub Container Registry. Contiene el servidor y sus dependencias, y su proyecto se monta en /app en tiempo de ejecución, así que una sola imagen sirve para cualquier tarea:
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestLa imagen existe porque las dependencias básicas de Potato incluyen numpy, pandas, scipy y scikit-learn. Instalarlas en una VM pequeña es lento en el mejor de los casos. En una máquina de 1 GB la compilación se interrumpe por falta de memoria, y en una de 2 GB tarda de cinco a diez minutos en cada nuevo despliegue. Descargar la imagen tarda menos de un minuto.
Importan dos etiquetas. latest lleva las dependencias básicas y ocupa unos 840 MB, y latest-all añade los SDK de IA, los formatos de documento, la exportación y los inicios de sesión OAuth, con unos 1,5 GB. Ambas se compilan para linux/amd64 y linux/arm64. Fije una versión en lugar de latest para un estudio que durará meses, porque latest cambia sin que usted lo note.
Defina POTATO_SECRET_KEY en todo lo que dure más de una sola ejecución. Sin ella, la clave de firma de sesiones de Flask es aleatoria en cada proceso, así que cada reinicio cierra la sesión de todos los anotadores. POTATO_CONFIG elige una ruta de configuración dentro del contenedor, PORT fija el puerto de escucha (7860 por defecto) y GUNICORN_TIMEOUT fija el tiempo de espera del worker en segundos.
La imagen se ejecuta como uid 1000, y un bind mount conserva la propiedad del host, así que en Linux el contenedor a menudo no puede escribir en un directorio que usted creó. El servidor lo comprueba al arrancar y se niega a ejecutarse en lugar de fallar a mitad del arranque. Entregue el directorio al usuario del contenedor con sudo chown -R 1000:1000 myproject, o ejecute como usted mismo con docker run --user "$(id -u):$(id -g)", que además deja las anotaciones a su nombre. Docker Desktop en macOS y Windows ignora la propiedad en los bind mounts, así que una configuración que funciona en un Mac puede fallar igualmente en un servidor Linux.
Instalación en una VM
En una máquina que administra directamente, instale Potato en un entorno virtual y deje que systemd se encargue del proceso.
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 --versionEjecute la aplicación con gunicorn con un worker y varios threads. La factoría de la aplicación está en potato.flask_server:create_app y recibe la ruta de configuración:
gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
--chdir /opt/potato/projects/my_project \
"potato.flask_server:create_app('config.yaml')"Cree /etc/systemd/system/potato.service para que el servidor vuelva a arrancar tras un reinicio o una caída:
[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.targetHabilítelo e inícielo:
sudo systemctl enable potato
sudo systemctl start potato
sudo systemctl status potatoEl EnvironmentFile guarda los secretos que no deben estar en la configuración:
# /opt/potato/.env
POTATO_SECRET_KEY=a-long-random-value
OPENAI_API_KEY=sk-...Genere la clave secreta una sola vez con openssl rand -hex 32 y consérvela durante todo el estudio. Cambiarla invalida todas las sesiones y cierra la sesión de todos los anotadores.
nginx y TLS
Potato escucha en localhost y nginx termina TLS delante de él. El límite de subida importa en proyectos de imagen y audio, donde el límite de cuerpo por defecto de 1 MB rechaza archivos multimedia sin que el anotador vea el fallo.
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;
}
}Habilite el sitio y obtenga un certificado:
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.timerCierre todo lo demás en el firewall, porque el puerto de Potato no debe ser accesible directamente una vez que hay un proxy delante:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enableServir varios proyectos bajo un mismo nombre de host
Las instituciones suelen exponer varios servidores de Potato a través de un único endpoint HTTPS público, asignando rutas a puertos locales. La interfaz de anotación pide recursos y guarda mediante URL relativas a la raíz, así que un servidor montado en /app1 necesita conocer su prefijo; si no, el CSS da 404 y los autoguardados fallan con "annotations not saved". Potato lee el prefijo del SCRIPT_NAME de WSGI, y hay dos formas de fijarlo.
Use POTATO_PROXY_FIX=1 cuando controle el proxy y este pueda enviar X-Forwarded-Prefix. Use POTATO_URL_PREFIX=/app1 cuando la configuración del proxy no dependa de usted. El proxy debe seguir quitando el prefijo antes de reenviar, para que Flask siga recibiendo rutas sin prefijo:
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 confía en las cabeceras reenviadas, así que active POTATO_PROXY_FIX solo cuando la aplicación sea accesible exclusivamente a través del proxy de confianza. Si el puerto interno también es accesible directamente, un cliente puede falsificar el prefijo y envenenar las URL que genera Potato.
La configuración que necesita un proyecto en producción
Cuatro ajustes separan una configuración de producción de una local: una clave de firma estable, la exigencia de contraseña, una duración de sesión explícita y un directorio de salida del que tenga respaldo.
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 limita el tiempo de inactividad y no la duración de la jornada, porque el reloj se reinicia con cada petición. El valor por defecto de 480 conviene a anotadores que trabajan una jornada completa; redúzcalo cuando la máquina es compartida. Ponga allow_all_users: false y enumere a sus anotadores siempre que el servidor sea accesible desde internet, para que un desconocido que encuentre la URL no pueda registrarse y empezar a escribir en su dataset.
Comprobaciones de salud y logs
/health es una sonda de disponibilidad sin autenticación para contenedores y balanceadores de carga, y a propósito no informa casi nada, porque todo lo que dice es público:
curl http://localhost:8000/health
# {"status": "ok"} once the state managers exist
# {"status": "starting"} 503 while the server is still coming upEl healthcheck de un contenedor y el sondeo posterior a la creación de un proveedor de despliegue se basan en esa transición. Los nombres de tarea, los recuentos de ítems y los recuentos de anotadores vienen en cambio de /admin/health, que necesita la clave de API de administración.
Los logs pasan por el proceso, así que systemd los recoge:
sudo journalctl -u potato -fSuba el nivel desde la configuración con verbose: true, o active las partes del registro de depuración que necesite con debug_log, que acepta all, ui, server o none. Deje ambos desactivados durante un estudio real, porque el registro de depuración es tan detallado que puede llenar un disco pequeño a lo largo de una ejecución larga.
Qué respaldar
Tres cosas del directorio del proyecto no se pueden regenerar. La salida de anotaciones bajo output_annotation_dir es el estudio. project.sqlite guarda los memos, el libro de códigos, los casos, el índice de búsqueda y el flujo de revisión. datasets.sqlite guarda los registros del dataset. Todo lo demás se puede reconstruir a partir de sus archivos de datos y su configuración.
#!/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 -deleteEjecútelo cada noche desde crontab y copie el archivo fuera de la máquina:
0 2 * * * /opt/potato/backup.shDimensionar la máquina
La memoria es el recurso que crece con el dataset, porque el pool de ítems se mantiene en el proceso. Todo lo demás se ha hecho perezoso, así que la pila de embeddings y sentence-transformers no se importa al arrancar salvo que una función la necesite. Con el dataset de referencia de las notas de la versión v2.6.0, la memoria residente bajó de unos 750 MB a unos 365 MB, y el arranque con 50.000 ítems pasó de unos 10 segundos a unos 5,7 segundos.
El coste por ítem es pequeño y medible. Un ítem de visión con una URL de imagen y dos campos ocupa unos 932 bytes, y un ítem de texto de unos 540 caracteres únicos, unos 1430 bytes. Un millón de ítems de texto supone por tanto unos 1,43 GB residentes, que es la cifra con la que presupuestar.
Para corpus en los que eso importa, los contenidos de los ítems pueden vivir en un archivo SQLite con una pequeña caché en memoria:
item_store:
backend: paged
cache_size: 2048Añada este bloque a la configuración anterior. Lea la medición antes de activarlo. La paginación ahorra un 28% en ítems de visión y un 51% en texto, no un orden de magnitud, y a cambio cargar el corpus tarda unas 2,5 veces más. Por debajo del millón de ítems solo le da un servidor más lento y ningún beneficio.
Prefiera una máquina con más RAM a dividir un proyecto entre servidores, porque dividirlo es el fallo multiproceso descrito al principio de esta guía. Cuando una sola máquina de verdad no puede alojar el corpus, reparta el trabajo. La asignación por lotes da a cada anotador una porción, per_annotator_quota limita cuánto se asigna a una persona y un data_directory con vigilancia de directorio le permite añadir archivos mientras el servidor está en marcha.
Solución de problemas
El puerto ya está en uso. Encuentre el proceso y deténgalo, en lugar de arrancar un segundo Potato en otro puerto sobre el mismo directorio de proyecto:
sudo lsof -i :8000Permiso denegado en el directorio del proyecto. El usuario del servicio debe ser su propietario, y en Docker ese usuario es el uid 1000:
sudo chown -R potato:potato /opt/potatoLos anotadores pierden la sesión tras cada reinicio. Falta POTATO_SECRET_KEY o el valor de configuración secret_key, así que la clave de firma se regenera en cada arranque.
Los recursos dan 404 y los autoguardados fallan detrás de un proxy. El servidor no conoce su prefijo de URL. Defina POTATO_PROXY_FIX=1 o POTATO_URL_PREFIX, y compruebe que el proxy quita el prefijo antes de reenviar.
Lecturas adicionales
- Configuración de producción y Proxy inverso en este sitio
- Running Potato in Docker como referencia de la imagen
- Scaling and Large Datasets con las mediciones detrás de las recomendaciones de dimensionamiento
- Estudios de anotación en Prolific cuando el servidor ya sea accesible