Configuración de producción
Despliega Potato en producción con gunicorn, nginx o Apache, terminación TLS, Docker, persistencia de sesiones y el control de acceso que necesita un servidor compartido.
Despliega Potato para tareas de anotación reales con varios usuarios, persistencia de datos y una seguridad adecuada.
Configuración del servidor
Configuración básica de producción
server:
host: 0.0.0.0
port: 8000
debug: falseEl bloque server acepta host, port y debug, y nada más. El número de workers y los tiempos de espera corresponden al servidor WSGI que ejecuta Potato, no al archivo de configuración. Mantén debug desactivado en producción, porque el modo de depuración abre las páginas de administración sin clave a las solicitudes que vienen de la propia máquina del servidor, y en un host desplegado eso incluye su proxy inverso.
Uso de HTTPS
La configuración de Potato no incluye ajustes de TLS, así que los certificados no se configuran ahí. Termina TLS en el proxy inverso situado delante de Potato, que es lo que configuran las secciones de nginx y Apache más abajo, o entrega el certificado directamente a gunicorn como se muestra en "Con SSL".
Ejecución con Gunicorn
Ejecuta Potato con gunicorn con exactamente un worker, y aumenta el número de hilos cuando necesites más concurrencia:
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:8000 \
--timeout 120El worker único es un requisito de corrección, no una decisión de ajuste. Potato mantiene en memoria, dentro del proceso, su conjunto de elementos, su cola de asignación y el estado de cada anotador. Un segundo worker recibe su propia copia de los tres, de modo que reparte instancias que el primer worker ya asignó, y como user_state.json se reescribe completo en cada guardado, el worker que guarda en último lugar descarta las anotaciones del otro. Nada informa de la pérdida, y las anotaciones simplemente desaparecen. Los hilos comparten una sola copia de ese estado, así que --threads aumenta la concurrencia de forma segura y --workers no.
Con 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.pemConfiguración del proxy inverso
Configuración 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;
}
}Configuración de 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>Montaje bajo una ruta URL
Servir Potato en https://host/app1/ en lugar de en la raíz de un dominio requiere un paso adicional. La página de anotación solicita /static/... y /updateinstance en relación con la raíz pública, así que bajo un prefijo de ruta los recursos devuelven 404 y las anotaciones no se guardan, aunque la estructura de la página se siga mostrando.
Indica a Potato su prefijo público. Cuando controlas el proxy y este puede enviar cabeceras de reenvío:
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;
}Activa POTATO_PROXY_FIX solo cuando la aplicación sea accesible exclusivamente a través de ese proxy. Confía en las cabeceras de reenvío, así que un cliente que pueda llegar directamente al puerto interno podría falsificar el prefijo y contaminar todas las URL generadas.
Cuando no puedes añadir cabeceras pero conoces la ruta de montaje, configúrala directamente. El proxy debe seguir eliminando el prefijo antes de reenviar:
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000Haz la prueba iniciando sesión en un navegador sin sesión abierta, ya que el formulario de inicio de sesión es la primera página que se rompe cuando falta el prefijo.
Los visores de agente en vivo y de programación en vivo transmiten mediante Server-Sent Events, que un proxy con búfer retiene hasta que termina el flujo. Desactiva el búfer en esas rutas:
location /app1/api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}Despliegue con Docker
Potato publica una imagen precompilada en GitHub Container Registry, ghcr.io/davidjurgens/potato. Contiene Potato y sus dependencias, y nada más. Tu carpeta de proyecto, con config.yaml y sus datos, se monta en /app, 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 etiqueta latest y las etiquetas de versión, como 2.12.1, incluyen solo las dependencias básicas. Las etiquetas -all (latest-all, 2.12.1-all) añaden los SDK de IA, los lectores de formatos y los extras de exportación y autenticación, y dejan fuera los modelos de visión.
El contenedor sirve la tarea con un worker de gunicorn y ocho hilos, por la razón que se da en la sección de gunicorn más arriba. Si GUNICORN_WORKERS vale más de 1, el contenedor se detiene al arrancar salvo que también se defina POTATO_ALLOW_MULTIWORKER=1, lo que en un estudio real hace perder anotaciones. Aumenta GUNICORN_THREADS para tener más concurrencia. POTATO_CONFIG indica un archivo de configuración distinto de config.yaml, y PORT cambia el puerto dentro del contenedor, que por defecto es 7860.
El contenedor se ejecuta con el uid 1000 y escribe en /app la salida de anotaciones, potato.log y sus bases de datos SQLite. En Linux, si la carpeta del proyecto montada con bind mount pertenece a otro usuario, el servidor se detiene al arrancar con un error que nombra el problema. Asigna la carpeta al uid 1000 con sudo chown -R 1000:1000 myproject. Docker Desktop en Mac ignora la propiedad de los bind mounts, así que el error solo aparece en Linux.
Los argumentos que siguen al nombre de la imagen sustituyen el comando del servidor, lo que resulta útil para comprobaciones puntuales:
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-stoppedDentro de la red de Compose, nginx llega a Potato por el nombre del servicio en el puerto del contenedor, así que el bloque upstream de la sección de proxy inverso anterior pasa a ser server potato:7860; en lugar de server 127.0.0.1:8000;.
Como la carpeta del proyecto se monta en lugar de copiarse en una imagen, las anotaciones que Potato escribe en ella permanecen en el host cuando se reemplaza el contenedor.
Control de acceso
Restringir quién puede registrarse
Potato permite el autorregistro por defecto. En un servidor público, indica quién puede registrarse:
user_config:
allow_all_users: false
users:
- annotator1@example.com
- annotator2@example.comAmbas claves van bajo user_config. Escritas en el nivel superior se ignoran, y el resultado parece una lista de acceso pero admite a todo el mundo.
Acceso de administrador
admin_api_key es una clave compartida que se envía en la cabecera X-API-Key. Si no la defines, Potato genera una y la escribe en {task_dir}/admin_api_key.txt, así que en un servidor de producción defínela tú y mantenla fuera del repositorio:
admin_api_key: "your-shared-key"
rbac:
enabled: true
user_role_assignments:
lead@example.com: adminCon rbac configurado, las personas indicadas llegan a /admin iniciando sesión con normalidad, en lugar de pasarse la clave compartida. Consulta Gestión de usuarios para ver el conjunto completo de permisos.
Sesiones
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200secret_key firma la cookie de sesión y también puede venir de POTATO_SECRET_KEY. Sin ella, Potato genera una clave nueva en cada arranque, así que cada reinicio cierra la sesión de todos a mitad de la tarea. En un servidor que se reinicia con cada despliegue, este es el ajuste que con más frecuencia cuesta anotaciones.
session_timeout_minutes borra una sesión que lleva ese tiempo inactiva, y el contador vuelve a empezar con cada petición. Potato 2.11 es la primera versión que lo aplica, con un valor por defecto de 480 minutos. Con 60, como en este ejemplo, un anotador que se ausenta una hora tiene que volver a iniciar sesión cuando regresa.
Salida de anotaciones
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300Potato escribe un directorio por anotador bajo output_annotation_dir, y cada uno contiene el user_state.json de esa persona. export_annotation_format indica los formatos que escribe la exportación periódica, y un valor vacío significa que no hay ninguna exportación automática. auto_export_interval es el intervalo entre exportaciones en segundos, y su valor por defecto es 60.
Backend de base de datos
Por defecto, las anotaciones se guardan en disco. Para despliegues que necesitan una base de datos, database.type acepta mysql o file:
database:
type: mysql
host: localhost
port: 3306
database: potato_db
username: potato
password: ${DB_PASSWORD}Las claves son database y username, no name y user. La cadena de conexión también puede venir de POTATO_DB_CONNECTION, lo que mantiene la credencial fuera del archivo de configuración.
Instala el driver con pip install 'potato-annotation[mysql]'. Antes de la 2.10.2 el backend MySQL nunca se activaba, y una configuración que lo indicaba guardaba las anotaciones en disco sin avisar. Desde la 2.10.2, si falta el driver o no se puede llegar al servidor, el arranque se detiene, y la exportación, las copias de seguridad y potato deploy pull leen la base de datos.
Comprobaciones de salud y registro
Potato sirve GET /health en todos los despliegues, y la ruta no requiere autenticación, así que funciona como sonda de disponibilidad para un balanceador de carga o un orquestador:
curl -sf https://annotation.example.com/healthEl nivel de detalle del registro se fija en la configuración o en la línea de comandos, no en un bloque de registro:
verbose: true
debug_log: serverdebug_log acepta all, ui, server o none. La variable de entorno POTATO_LOGGERS limita la salida a los loggers indicados cuando estás investigando un solo subsistema.
Escalabilidad
Potato se ejecuta como un único proceso que mantiene todo el conjunto de elementos en memoria, así que escala la máquina, no el número de servidores. Ejecutar varias réplicas detrás de un balanceador de carga no reparte el trabajo. Cada réplica mantiene su propia copia de la cola de asignación y del estado de los anotadores, lo que reproduce exactamente la pérdida de datos descrita en la sección de gunicorn.
La memoria es el recurso que crece con el conjunto de datos. En torno al millón de elementos, los datos de cada elemento pueden pasar a una caché SQLite:
item_store:
backend: paged # default: memory
cache_size: 2048Mide antes de activarlo. La paginación ahorra alrededor de un 28% en elementos de visión y un 51% en texto, no un orden de magnitud, y hace que la carga del corpus sea unas 2.5 veces más lenta. El archivo es una caché que se reconstruye a partir de los archivos de datos en cada arranque, así que borrarlo es seguro.
En proyectos grandes, las palancas eficaces son limitar lo que se asigna a cada anotador con per_annotator_quota, repartir el corpus entre cohortes con batch_assignment e ir añadiendo archivos de datos con el tiempo mediante data_directory, en lugar de cargarlo todo al arrancar.
Estrategia de respaldo
Un respaldo tiene que cubrir más que el directorio de salida de anotaciones. El directorio de la tarea también contiene hasta tres bases de datos SQLite, y ninguna se puede reconstruir a partir de los archivos de datos: project.sqlite (los memos, el libro de códigos, el flujo de revisión), datasets.sqlite y engine.sqlite cuando se usa el motor de etiquetado. Potato guarda en user_config.json las cuentas creadas mediante el formulario de registro. Las bases de datos funcionan en modo WAL mientras el servidor escribe en ellas, así que un tar o un cp de un archivo de base de datos puede producir una copia corrupta o sin el trabajo reciente, y ninguno de los dos comandos te avisa.
El respaldo integrado
Desde la versión 2.10, el bloque backup de Potato copia fuera del servidor la salida de anotaciones e instantáneas .backup de las bases de datos, y las restaura cuando el servidor arranca con el disco vacío. La restauración recupera la lista de cuentas junto con las anotaciones, y nunca sobrescribe anotaciones que ya estén en el disco. El bloque siguiente envía la copia a un bucket de S3 cada cinco minutos, el intervalo por defecto, así que la pérdida de un disco cuesta a los anotadores como mucho cinco minutos de trabajo:
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 el destino de la copia a cualquier servicio que hable la API de S3, como un almacenamiento de objetos de la universidad, MinIO, Cloudflare R2 o Backblaze B2. Las credenciales salen de POTATO_S3_ACCESS_KEY_ID y POTATO_S3_SECRET_ACCESS_KEY en el entorno del servidor, o de la cadena habitual de boto3. Una clave limitada a ese único bucket restringe lo que el servidor puede alcanzar. Un destino type: huggingface con un repo_id envía la copia a un dataset privado de Hugging Face, usando HF_TOKEN. Los dos destinos necesitan el extra hosting, pip install 'potato-annotation[hosting]'.
Una copia que se queda en tu propio almacenamiento
Cuando un acuerdo de uso de datos obliga a mantener los datos en el servidor, una tarea nocturna puede copiarlos a un almacenamiento local o montado. El script siguiente archiva el directorio de salida, crea una instantánea de cada base de datos con el comando .backup de SQLite en lugar de copiar el archivo y conserva las copias de siete días. Necesita la herramienta de línea de comandos 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 {} +Ejecútalo con cron a las 2 de la madrugada cada noche:
0 2 * * * /var/potato/backup.shUna copia nocturna puede perder hasta un día de trabajo, así que ejecútala con más frecuencia durante un estudio con muchos anotadores. Antes de que empiece el estudio, restaura una copia en un servidor de prueba y comprueba que la última anotación está ahí.
Lista de verificación de seguridad
- Terminar TLS en el proxy con un certificado válido
- Definir
secret_key, oPOTATO_SECRET_KEY, para que las sesiones sobrevivan a un reinicio - Definir
admin_api_keyen lugar de desplegar el archivo generado - Restringir el registro con
user_config.users - Mantener
debugdesactivado, ya que el modo de depuración deja que las solicitudes del proxy inverso lleguen a las páginas de administración sin clave - Ejecutar gunicorn con un solo worker
- Guardar las credenciales en variables de entorno, no en la configuración
- Activar
POTATO_PROXY_FIXsolo detrás de un proxy de confianza - Respaldar el directorio de salida, el archivo de cuentas y las bases de datos, y probar una restauración
- Revisar quién tiene un rol de administrador
Variables de entorno
# .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_dbGenera una clave de firma con:
python3 -c 'import secrets; print(secrets.token_hex(32))'Carga en Docker Compose:
services:
potato:
env_file:
- .envSolución de problemas
Revisar logs
# Gunicorn logs
journalctl -u potato -f
# Docker logs
docker-compose logs -f potatoProblemas comunes
502 Bad Gateway: Verifica que Potato esté en ejecución y escuchando en el puerto al que reenvía el proxy.
Todos pierden la sesión tras un despliegue: secret_key no está definido, así que se generó una nueva clave de firma al arrancar.
Faltan anotaciones: Revisa el número de workers. Con más de un worker de gunicorn se descartan anotaciones sin ningún aviso.
Recursos con 404 bajo un prefijo de ruta: Define POTATO_PROXY_FIX o POTATO_URL_PREFIX, y confirma que el proxy elimina el prefijo antes de reenviar.
Lentitud con un corpus grande: Añade memoria antes de añadir servidores, y considera item_store.backend: paged a partir de alrededor de un millón de elementos.
Próximos pasos
- Gestión de usuarios - Listas de acceso, roles y cuotas
- Crowdsourcing - Prolific, SONA y otros paneles
- Formatos de datos - Almacenamiento y exportación
Para detalles de implementación, consulta la guía de proxy inverso y la guía de escalado.