Skip to content

Configurazione di produzione

Distribuisci Potato in produzione con gunicorn, nginx o Apache, terminazione TLS, Docker, persistenza delle sessioni e il controllo degli accessi di cui ha bisogno un server condiviso.

Distribuisci Potato per attività di annotazione reali con più utenti, persistenza dei dati e una sicurezza adeguata.

Configurazione del server

Configurazione di base per la produzione

yaml
server:
  host: 0.0.0.0
  port: 8000
  debug: false

Il blocco server accetta host, port e debug, e nient'altro. Il numero di worker e i timeout spettano al server WSGI che esegue Potato, non al file di configurazione. In produzione tieni debug disattivato, perché la modalità debug apre le pagine di amministrazione senza chiave alle richieste che arrivano dalla macchina stessa del server, e su un host in produzione tra queste rientra il suo reverse proxy.

Utilizzo di HTTPS

La configurazione di Potato non contiene impostazioni TLS, quindi i certificati non si configurano lì. Termina TLS sul reverse proxy davanti a Potato, come fanno le sezioni su nginx e Apache più sotto, oppure passa il certificato direttamente a gunicorn come mostrato in "Con SSL".

Esecuzione con Gunicorn

Esegui Potato sotto gunicorn con un solo worker, e aumenta il numero di thread quando ti serve più concorrenza:

bash
gunicorn "potato.flask_server:create_app('config.yaml')" \
  --workers 1 \
  --threads 8 \
  --bind 0.0.0.0:8000 \
  --timeout 120

Il worker singolo è un requisito di correttezza, non una scelta di ottimizzazione. Potato tiene in memoria, dentro il processo, il pool degli item, la coda di assegnazione e lo stato di ogni annotatore. Un secondo worker riceve una propria copia di tutti e tre, quindi distribuisce istanze che il primo worker ha già assegnato, e poiché user_state.json viene riscritto per intero a ogni salvataggio, il worker che salva per ultimo cancella le annotazioni dell'altro. Nessun messaggio segnala la perdita, e le annotazioni semplicemente spariscono. I thread condividono un'unica copia di quello stato, per cui --threads aumenta la concorrenza in modo sicuro, cosa che --workers non fa.

Con SSL

bash
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.pem

Configurazione del reverse proxy

Configurazione Nginx

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;
    }
}

Configurazione Apache

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>

Montaggio sotto un percorso URL

Servire Potato su https://host/app1/ invece che alla radice di un dominio richiede un passaggio in più. La pagina di annotazione richiede /static/... e /updateinstance rispetto alla radice pubblica, quindi sotto un prefisso di percorso le risorse restituiscono 404 e le annotazioni non vengono salvate, mentre l'interfaccia viene comunque visualizzata.

Comunica a Potato il suo prefisso pubblico. Quando controlli il proxy e questo può inviare header forwarded:

bash
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000
nginx
location /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;
}

Abilita POTATO_PROXY_FIX solo quando l'app è raggiungibile esclusivamente attraverso quel proxy. La variabile si fida degli header forwarded, quindi un client in grado di raggiungere direttamente la porta interna potrebbe falsificare il prefisso e corrompere ogni URL generato.

Quando non puoi aggiungere header ma conosci il percorso di montaggio, impostalo direttamente. Il proxy deve comunque rimuovere il prefisso prima di inoltrare la richiesta:

bash
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000

Per verificare, accedi da un browser senza sessione, perché il modulo di login è la prima pagina che un prefisso mancante rompe.

I visualizzatori live agent e live coding trasmettono tramite Server-Sent Events, che un proxy con buffering trattiene finché lo stream non termina. Disattiva il buffering su quelle route:

nginx
location /app1/api/ {
    proxy_pass            http://127.0.0.1:8000/api/;
    proxy_set_header      Host $host;
    proxy_buffering       off;
    proxy_read_timeout    3600s;
}

Distribuzione con Docker

Potato pubblica un'immagine precompilata su GitHub Container Registry, ghcr.io/davidjurgens/potato. Contiene Potato e le sue dipendenze e nient'altro. La cartella del progetto, con config.yaml e i suoi dati, viene montata su /app, quindi un'unica immagine serve qualsiasi task:

bash
docker run -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  -e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
  ghcr.io/davidjurgens/potato:latest

Il tag latest e i tag di release, come 2.12.1, includono solo le dipendenze di base. I tag -all (latest-all, 2.12.1-all) aggiungono gli SDK di IA, i lettori di formati e gli extra per l'esportazione e l'autenticazione, e lasciano fuori i modelli di visione.

Il container serve il task con un worker gunicorn e otto thread, per il motivo spiegato nella sezione su gunicorn più sopra. Impostare GUNICORN_WORKERS a un valore superiore a 1 arresta il container all'avvio, a meno che non sia impostato anche POTATO_ALLOW_MULTIWORKER=1, il che in uno studio reale fa perdere annotazioni. Aumenta GUNICORN_THREADS per avere più concorrenza. POTATO_CONFIG indica un file di configurazione diverso da config.yaml, e PORT cambia la porta all'interno del container, che di default è 7860.

Il container gira come uid 1000 e scrive in /app l'output delle annotazioni, potato.log e i suoi database SQLite. Su Linux, se la cartella del progetto montata con bind mount appartiene a un altro utente, il server si ferma all'avvio con un errore che indica il problema. Assegna la cartella all'uid 1000 con sudo chown -R 1000:1000 myproject. Docker Desktop su Mac ignora la proprietà dei bind mount, quindi l'errore compare solo su Linux.

Gli argomenti dopo il nome dell'immagine sostituiscono il comando del server, cosa utile per controlli una tantum:

bash
docker run -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strict

Docker Compose

yaml
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-stopped

All'interno della rete Compose, nginx raggiunge Potato tramite il nome del servizio sulla porta del container, quindi il blocco upstream della sezione sul reverse proxy più sopra diventa server potato:7860; invece di server 127.0.0.1:8000;.

Poiché la cartella del progetto viene montata e non copiata in un'immagine, le annotazioni che Potato vi scrive restano sull'host quando il container viene sostituito.

Controllo degli accessi

Limitare chi può registrarsi

Per impostazione predefinita Potato consente l'autoregistrazione. Su un server pubblico, elenca chi può registrarsi:

yaml
user_config:
  allow_all_users: false
  users:
    - annotator1@example.com
    - annotator2@example.com

Entrambe le chiavi vanno sotto user_config. Scritte al livello superiore vengono ignorate, e il risultato sembra una lista di accesso ma lascia entrare chiunque.

Accesso amministratore

admin_api_key è una chiave condivisa inviata nell'header X-API-Key. Se non la imposti, Potato ne genera una e la scrive in {task_dir}/admin_api_key.txt, quindi su un host di produzione impostala tu e tienila fuori dal repository:

yaml
admin_api_key: "your-shared-key"
 
rbac:
  enabled: true
  user_role_assignments:
    lead@example.com: admin

Con rbac configurato, le persone indicate raggiungono /admin accedendo normalmente, senza doversi passare la chiave condivisa. Consulta Gestione utenti per l'insieme completo dei permessi.

Sessioni

yaml
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200

secret_key firma il cookie di sessione e può provenire anche da POTATO_SECRET_KEY. Senza di essa, Potato genera una nuova chiave a ogni avvio, quindi ogni riavvio disconnette tutti a metà del lavoro. Su un server che si riavvia a ogni deploy, è l'impostazione che più spesso costa annotazioni.

session_timeout_minutes chiude una sessione rimasta inattiva per quel tempo, e il conteggio riparte a ogni richiesta. Potato 2.11 è la prima release che lo applica, con un default di 480 minuti. Con 60, come in questo esempio, un annotatore che si allontana per un'ora deve accedere di nuovo quando torna.

Output delle annotazioni

yaml
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300

Potato scrive una directory per annotatore sotto output_annotation_dir, ognuna con il user_state.json di quella persona. export_annotation_format indica i formati scritti dall'esportazione periodica, e un valore vuoto significa nessuna esportazione automatica. auto_export_interval è l'intervallo tra un'esportazione e l'altra in secondi, con valore predefinito 60.

Backend database

Per impostazione predefinita le annotazioni risiedono su disco. Per le distribuzioni che richiedono un database, database.type accetta mysql o file:

yaml
database:
  type: mysql
  host: localhost
  port: 3306
  database: potato_db
  username: potato
  password: ${DB_PASSWORD}

Le chiavi sono database e username, non name e user. Una stringa di connessione può provenire invece da POTATO_DB_CONNECTION, così la credenziale resta fuori dal file di configurazione.

Installa il driver con pip install 'potato-annotation[mysql]'. Prima della 2.10.2 il backend MySQL non si attivava mai, e una configurazione che lo indicava salvava le annotazioni su disco senza segnalarlo. Dalla 2.10.2 un driver mancante o un server irraggiungibile blocca l'avvio, e l'esportazione, i backup e potato deploy pull leggono il database.

Health check e logging

Potato serve GET /health in ogni distribuzione, e la route non richiede autenticazione, quindi funziona come readiness probe per un load balancer o un orchestratore:

bash
curl -sf https://annotation.example.com/health

Il livello di dettaglio dei log si imposta nella configurazione o dalla riga di comando, non in un blocco di logging:

yaml
verbose: true
debug_log: server

debug_log accetta all, ui, server o none. La variabile d'ambiente POTATO_LOGGERS limita l'output a logger specifici quando stai indagando su un singolo sottosistema.

Scalabilità

Potato gira come un unico processo che tiene in memoria l'intero insieme di item, quindi va potenziata la macchina, non aumentato il numero di server. Eseguire più repliche dietro un load balancer non divide il lavoro. Ogni replica mantiene la propria copia della coda di assegnazione e dello stato degli annotatori, il che riproduce esattamente la perdita di dati descritta più sopra nella sezione su gunicorn.

La memoria è la risorsa che cresce con il dataset. Intorno al milione di item, i payload possono passare a una cache SQLite:

yaml
item_store:
  backend: paged      # default: memory
  cache_size: 2048

Misura prima di attivarla. La paginazione fa risparmiare circa il 28% sugli item di visione e il 51% sul testo, non un ordine di grandezza, e rende il caricamento del corpus circa 2,5 volte più lento. Il file è una cache, ricostruita dai file di dati a ogni avvio, quindi eliminarlo è sicuro.

Per i progetti grandi, le leve efficaci sono limitare quanto viene assegnato a ciascun annotatore con per_annotator_quota, suddividere il corpus tra coorti con batch_assignment e aggiungere file di dati nel tempo con data_directory invece di caricare tutto all'avvio.

Strategia di backup

Un backup deve coprire più della sola directory di output delle annotazioni. La directory del task contiene anche fino a tre database SQLite, nessuno dei quali si può ricostruire dai file di dati: project.sqlite (i memo, il codebook, il workflow di revisione), datasets.sqlite e engine.sqlite quando è in uso il motore di etichettatura. Potato salva gli account creati tramite il modulo di registrazione in user_config.json. I database funzionano in modalità WAL mentre il server vi scrive, quindi un tar o un cp di un file di database può produrre una copia corrotta o priva del lavoro recente, e nessuno dei due comandi ti avvisa.

Il backup integrato

Dalla versione 2.10, il blocco backup di Potato copia fuori dal server l'output delle annotazioni e gli snapshot .backup dei database, e li ripristina quando il server si avvia con il disco vuoto. Il ripristino riporta l'elenco degli account insieme alle annotazioni, e non sovrascrive mai le annotazioni già presenti sul disco. Il blocco qui sotto invia la copia a un bucket S3 ogni cinque minuti, l'intervallo predefinito, quindi la perdita di un disco costa agli annotatori al massimo cinque minuti di lavoro:

yaml
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 S3

endpoint_url indirizza la destinazione verso qualsiasi servizio che parli l'API S3, come un object store universitario, MinIO, Cloudflare R2 o Backblaze B2. Le credenziali provengono da POTATO_S3_ACCESS_KEY_ID e POTATO_S3_SECRET_ACCESS_KEY nell'ambiente del server, oppure dalla consueta catena di boto3. Una chiave limitata a quel solo bucket restringe ciò che il server può raggiungere. Una destinazione type: huggingface con un repo_id invia invece la copia a un dataset privato di Hugging Face, usando HF_TOKEN. Entrambe le destinazioni richiedono l'extra hosting, pip install 'potato-annotation[hosting]'.

Una copia che resta sul tuo storage

Quando un accordo sull'uso dei dati impone di tenerli sul server, un job notturno può invece copiarli su uno storage locale o montato. Lo script qui sotto archivia la directory di output, crea uno snapshot di ogni database con il comando .backup di SQLite invece di copiare il file e conserva sette giorni di copie. Richiede lo strumento a riga di comando sqlite3:

bash
#!/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 {} +

Eseguilo da cron alle 2 di ogni notte:

bash
0 2 * * * /var/potato/backup.sh

Una copia notturna può perdere fino a un giorno di lavoro, quindi eseguila più spesso durante uno studio con molti annotatori. Prima che lo studio inizi, ripristina una copia su un server di prova e controlla che l'ultima annotazione ci sia.

Lista di controllo della sicurezza

  • Termina TLS sul proxy con un certificato valido
  • Imposta secret_key, o POTATO_SECRET_KEY, così le sessioni sopravvivono a un riavvio
  • Imposta admin_api_key invece di usare il file generato
  • Limita la registrazione con user_config.users
  • Tieni debug disattivato, perché la modalità debug lascia arrivare le richieste del reverse proxy alle pagine di amministrazione senza chiave
  • Esegui gunicorn con un solo worker
  • Tieni le credenziali nelle variabili d'ambiente, non nella configurazione
  • Abilita POTATO_PROXY_FIX solo dietro un proxy fidato
  • Esegui il backup della directory di output, del file degli account e dei database, e prova un ripristino
  • Verifica chi ha un ruolo di amministratore

Variabili d'ambiente

bash
# .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_db

Genera una chiave di firma con:

bash
python3 -c 'import secrets; print(secrets.token_hex(32))'

Carica in Docker Compose:

yaml
services:
  potato:
    env_file:
      - .env

Risoluzione dei problemi

Controlla i log

bash
# Gunicorn logs
journalctl -u potato -f
 
# Docker logs
docker-compose logs -f potato

Problemi comuni

502 Bad Gateway: verifica che Potato sia in esecuzione e in ascolto sulla porta a cui il proxy inoltra le richieste.

Tutti disconnessi dopo un deploy: secret_key non è impostata, quindi all'avvio è stata generata una nuova chiave di firma.

Annotazioni mancanti: controlla il numero di worker. Più di un worker gunicorn cancella le annotazioni senza segnalarlo.

Risorse in 404 sotto un prefisso di percorso: imposta POTATO_PROXY_FIX o POTATO_URL_PREFIX, e verifica che il proxy rimuova il prefisso prima di inoltrare la richiesta.

Lentezza con un corpus grande: aggiungi memoria prima di aggiungere server, e valuta item_store.backend: paged oltre circa un milione di item.

Prossimi passi

Per i dettagli di implementazione, consulta la guida al reverse proxy e la guida alla scalabilità.