Skip to content

Produktionseinrichtung

Potato in Produktion betreiben, mit gunicorn, nginx oder Apache, TLS-Terminierung, Docker, dauerhaften Sitzungen und der Zugriffskontrolle, die ein gemeinsam genutzter Server braucht.

Stellen Sie Potato für reale Annotationsaufgaben mit mehreren Benutzern, dauerhafter Datenspeicherung und angemessener Sicherheit bereit.

Serverkonfiguration

Grundlegende Produktionskonfiguration

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

Der server-Block akzeptiert host, port und debug und sonst nichts. Worker-Anzahl und Timeouts gehören zu dem WSGI-Server, der Potato ausführt, und nicht in die Konfigurationsdatei. Lassen Sie debug in Produktion ausgeschaltet, weil der Debug-Modus die Admin-Seiten ohne Schlüssel für Anfragen vom Rechner des Servers selbst öffnet, und auf einem produktiv eingesetzten Host gehört dazu sein Reverse Proxy.

HTTPS verwenden

Die Konfiguration von Potato enthält keine TLS-Einstellungen, daher werden Zertifikate nicht dort eingerichtet. Terminieren Sie TLS am Reverse Proxy vor Potato, wie es die Abschnitte zu nginx und Apache weiter unten einrichten, oder übergeben Sie das Zertifikat direkt an gunicorn, wie unter „Mit SSL“ gezeigt.

Mit Gunicorn ausführen

Führen Sie Potato unter gunicorn mit genau einem Worker aus, und erhöhen Sie die Anzahl der Threads, wenn Sie mehr Nebenläufigkeit brauchen:

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

Der einzelne Worker ist eine Voraussetzung für korrekte Ergebnisse und keine Tuning-Entscheidung. Potato hält seinen Item-Pool, seine Zuweisungswarteschlange und den Zustand jedes Annotators im Speicher des Prozesses. Ein zweiter Worker bekommt von allen dreien eine eigene Kopie. Er verteilt deshalb Instanzen, die der erste Worker bereits zugewiesen hat, und weil user_state.json bei jedem Speichern vollständig neu geschrieben wird, verwirft der Worker, der zuletzt speichert, die Annotationen des anderen. Nichts meldet diesen Verlust, die Annotationen sind einfach weg. Threads teilen sich eine Kopie dieses Zustands, daher erhöht --threads die Nebenläufigkeit gefahrlos, --workers dagegen nicht.

Mit 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

Reverse Proxy einrichten

Nginx-Konfiguration

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

Apache-Konfiguration

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>

Unter einem URL-Pfad einbinden

Wenn Potato unter https://host/app1/ statt an der Wurzel einer Domain laufen soll, ist ein zusätzlicher Schritt nötig. Die Annotationsseite fordert /static/... und /updateinstance relativ zur öffentlichen Wurzel an. Unter einem Pfadpräfix liefern die Assets daher 404, und Annotationen werden nicht gespeichert, während das Grundgerüst der Seite weiterhin angezeigt wird.

Teilen Sie Potato sein öffentliches Präfix mit. Wenn Sie den Proxy kontrollieren und er Forwarded-Header senden kann:

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

Aktivieren Sie POTATO_PROXY_FIX nur, wenn die Anwendung ausschließlich über diesen Proxy erreichbar ist. Die Einstellung vertraut Forwarded-Headern. Ein Client, der den internen Port direkt erreicht, könnte das Präfix fälschen und damit jede erzeugte URL verfälschen.

Wenn Sie keine Header hinzufügen können, den Einbindungspfad aber kennen, setzen Sie ihn direkt. Der Proxy muss das Präfix trotzdem vor dem Weiterleiten entfernen:

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

Testen Sie, indem Sie sich in einem Browser ohne bestehende Sitzung anmelden, denn das Anmeldeformular ist die Seite, die bei fehlendem Präfix zuerst nicht mehr funktioniert.

Die Viewer für Live-Agenten und Live-Coding streamen über Server-Sent Events, die ein puffernder Proxy bis zum Ende des Streams zurückhält. Schalten Sie das Puffern auf diesen Routen ab:

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

Docker-Bereitstellung

Potato veröffentlicht ein fertiges Image in der GitHub Container Registry, ghcr.io/davidjurgens/potato. Es enthält Potato und seine Abhängigkeiten und sonst nichts. Ihr Projektordner mit config.yaml und den zugehörigen Daten wird unter /app eingebunden, sodass ein Image für jede Aufgabe genügt:

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

Der Tag latest und die Release-Tags wie 2.12.1 enthalten nur die Kernabhängigkeiten. Die -all-Tags (latest-all, 2.12.1-all) fügen die AI-SDKs, die Formatleser sowie die Extras für Export und Authentifizierung hinzu und lassen die Vision-Modelle weg.

Der Container bedient die Aufgabe mit einem gunicorn-Worker und acht Threads, aus dem Grund, der im Abschnitt zu gunicorn oben genannt ist. Wird GUNICORN_WORKERS auf mehr als 1 gesetzt, bricht der Container beim Start ab, sofern nicht zusätzlich POTATO_ALLOW_MULTIWORKER=1 gesetzt ist, was in einer echten Studie Annotationen verliert. Erhöhen Sie GUNICORN_THREADS für mehr Nebenläufigkeit. POTATO_CONFIG benennt eine andere Konfigurationsdatei als config.yaml, und PORT ändert den Port im Container, der sonst 7860 ist.

Der Container läuft als uid 1000 und schreibt die Annotationsausgabe, potato.log und seine SQLite-Datenbanken nach /app. Unter Linux hält ein per Bind-Mount eingebundener Projektordner, der einem anderen Benutzer gehört, den Server beim Start mit einer Fehlermeldung an, die das Problem benennt. Übertragen Sie den Ordner mit sudo chown -R 1000:1000 myproject an uid 1000. Docker Desktop auf dem Mac ignoriert die Eigentümerschaft von Bind-Mounts, daher tritt der Fehler nur unter Linux auf.

Argumente nach dem Image-Namen ersetzen den Serverbefehl, was für einmalige Prüfungen nützlich ist:

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

Innerhalb des Compose-Netzwerks erreicht nginx Potato über den Servicenamen und den Port des Containers. Der upstream-Block aus dem Abschnitt zum Reverse Proxy oben lautet daher server potato:7860; statt server 127.0.0.1:8000;.

Da der Projektordner eingebunden und nicht in ein Image kopiert wird, bleiben die Annotationen, die Potato dort hineinschreibt, auf dem Host erhalten, wenn der Container ersetzt wird.

Zugriffskontrolle

Einschränken, wer sich registrieren darf

Potato erlaubt standardmäßig die Selbstregistrierung. Legen Sie auf einem öffentlichen Server fest, wer sich anmelden darf:

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

Beide Schlüssel gehören unter user_config. Auf oberster Ebene geschrieben, werden sie ignoriert, und die Konfiguration sieht dann aus wie eine Zugriffsliste, lässt aber jeden zu.

Admin-Zugriff

admin_api_key ist ein gemeinsamer Schlüssel, der im Header X-API-Key gesendet wird. Wenn Sie ihn nicht setzen, erzeugt Potato einen und schreibt ihn nach {task_dir}/admin_api_key.txt. Setzen Sie ihn auf einem Produktionsserver daher selbst und halten Sie ihn aus dem Repository heraus:

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

Wenn rbac konfiguriert ist, gelangen namentlich eingetragene Personen über eine normale Anmeldung zu /admin, statt den gemeinsamen Schlüssel weiterzugeben. Die vollständigen Berechtigungen finden Sie unter Benutzerverwaltung.

Sitzungen

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

secret_key signiert das Sitzungs-Cookie und kann stattdessen aus POTATO_SECRET_KEY kommen. Fehlt der Schlüssel, erzeugt Potato bei jedem Start einen neuen, sodass jeder Neustart alle Benutzer mitten in der Aufgabe abmeldet. Auf einem Server, der bei jedem Deployment neu startet, ist dies die Einstellung, die am häufigsten Annotationen kostet.

session_timeout_minutes löscht eine Sitzung, die so lange inaktiv war, und die Uhr beginnt bei jeder Anfrage von vorn. Potato 2.11 ist das erste Release, das die Einstellung durchsetzt, mit einem Standardwert von 480 Minuten. Bei 60, wie in diesem Beispiel, muss sich ein Annotator, der eine Stunde weggeht, bei seiner Rückkehr erneut anmelden.

Annotationsausgabe

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

Potato schreibt unter output_annotation_dir ein Verzeichnis pro Annotator, das jeweils die user_state.json dieser Person enthält. export_annotation_format nennt die Formate, die der periodische Export schreibt, und ein leerer Wert bedeutet, dass überhaupt nicht automatisch exportiert wird. auto_export_interval ist der Abstand zwischen zwei Exporten in Sekunden und hat den Standardwert 60.

Datenbank-Backend

Annotationen liegen standardmäßig auf der Festplatte. Für Deployments, die eine Datenbank brauchen, akzeptiert database.type die Werte mysql oder file:

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

Die Schlüssel heißen database und username, nicht name und user. Ein Verbindungsstring kann stattdessen aus POTATO_DB_CONNECTION kommen, womit die Zugangsdaten nicht in der Konfigurationsdatei stehen.

Installieren Sie den Treiber mit pip install 'potato-annotation[mysql]'. Vor 2.10.2 wurde das MySQL-Backend nie aktiv, und eine Konfiguration, die es angab, speicherte Annotationen stillschweigend auf der Festplatte. Ab 2.10.2 bricht ein fehlender Treiber oder ein nicht erreichbarer Server den Start ab, und Export, Backups und potato deploy pull lesen die Datenbank.

Health-Checks und Protokollierung

Potato stellt GET /health in jedem Deployment bereit, und die Route erfordert keine Authentifizierung. Sie eignet sich daher als Readiness-Probe für einen Load Balancer oder einen Orchestrator:

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

Die Ausführlichkeit der Protokollierung wird in der Konfiguration oder auf der Kommandozeile festgelegt, nicht in einem eigenen Logging-Block:

yaml
verbose: true
debug_log: server

debug_log akzeptiert all, ui, server oder none. Die Umgebungsvariable POTATO_LOGGERS beschränkt die Ausgabe auf benannte Logger, wenn Sie ein einzelnes Subsystem untersuchen.

Skalierung

Potato läuft als einzelner Prozess, der den gesamten Item-Bestand im Speicher hält. Skalieren Sie daher die Maschine und nicht die Anzahl der Server. Mehrere Replikate hinter einem Load Balancer teilen die Arbeit nicht auf. Jedes Replikat hält eine eigene Kopie der Zuweisungswarteschlange und des Annotatorzustands, was genau den Datenverlust erzeugt, der oben im Abschnitt zu gunicorn beschrieben ist.

Der Speicher ist die Ressource, die mit dem Datensatz wächst. Ab etwa einer Million Items können die Nutzdaten in einen SQLite-Cache verlagert werden:

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

Messen Sie, bevor Sie dies aktivieren. Paging spart bei Vision-Items etwa 28 % und bei Text etwa 51 %, also keine Größenordnung, und es macht das Laden des Korpus etwa 2,5-mal langsamer. Die Datei ist ein Cache, der bei jedem Start aus den Datendateien neu aufgebaut wird, daher kann sie gefahrlos gelöscht werden.

Bei großen Projekten sind die wirksamen Hebel, mit per_annotator_quota zu begrenzen, wie viel einem einzelnen Annotator zugewiesen wird, den Korpus mit batch_assignment auf Kohorten aufzuteilen und mit data_directory Datendateien nach und nach hinzuzufügen, statt alles beim Start zu laden.

Backup-Strategie

Ein Backup muss mehr als das Ausgabeverzeichnis der Annotationen abdecken. Das Aufgabenverzeichnis enthält außerdem bis zu drei SQLite-Datenbanken, von denen sich keine aus den Datendateien neu aufbauen lässt: project.sqlite (Memos, das Codebuch, der Review-Workflow), datasets.sqlite und engine.sqlite, wenn die Labeling-Engine im Einsatz ist. Konten, die über das Registrierungsformular angelegt wurden, speichert Potato in user_config.json. Die Datenbanken laufen im WAL-Modus, während der Server in sie schreibt, daher kann ein tar oder cp einer Datenbankdatei eine Kopie erzeugen, die beschädigt ist oder der neuere Arbeit fehlt, und keiner der beiden Befehle warnt Sie davor.

Das eingebaute Backup

Ab Version 2.10 kopiert der backup-Block von Potato die Annotationsausgabe und .backup-Snapshots der Datenbanken vom Server weg und stellt sie wieder her, wenn der Server mit leerem Datenträger startet. Die Wiederherstellung bringt mit den Annotationen auch die Kontenliste zurück, und sie überschreibt nie Annotationen, die bereits auf dem Datenträger liegen. Der folgende Block schickt die Kopie alle fünf Minuten, dem Standardintervall, in einen S3-Bucket, sodass ein verlorener Datenträger die Annotatoren höchstens fünf Minuten Arbeit kostet:

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 richtet die Senke auf jeden Dienst, der die S3-API spricht, etwa einen Objektspeicher der Hochschule, MinIO, Cloudflare R2 oder Backblaze B2. Die Zugangsdaten stammen aus POTATO_S3_ACCESS_KEY_ID und POTATO_S3_SECRET_ACCESS_KEY in der Umgebung des Servers oder aus der üblichen Kette von boto3. Ein Schlüssel, der auf diesen einen Bucket beschränkt ist, begrenzt, worauf der Server zugreifen kann. Eine Senke type: huggingface mit einer repo_id schickt die Kopie stattdessen in ein privates Hugging-Face-Dataset und verwendet dafür HF_TOKEN. Beide Senken brauchen das Extra hosting, pip install 'potato-annotation[hosting]'.

Eine Kopie, die auf Ihrem eigenen Speicher bleibt

Wenn eine Datennutzungsvereinbarung die Daten auf dem Server hält, kann ein nächtlicher Job sie stattdessen auf lokalen oder eingebundenen Speicher kopieren. Das folgende Skript archiviert das Ausgabeverzeichnis, erstellt von jeder Datenbank mit dem SQLite-Befehl .backup einen Snapshot, statt die Datei zu kopieren, und bewahrt Kopien von sieben Tagen auf. Es braucht das Kommandozeilenwerkzeug 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 {} +

Führen Sie es jede Nacht um 2 Uhr über cron aus:

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

Eine nächtliche Kopie kann bis zu einem Tag Arbeit verlieren, führen Sie sie während einer Studie mit vielen Annotatoren also häufiger aus. Stellen Sie vor Beginn der Studie eine Kopie auf einem Testserver wieder her und prüfen Sie, ob die letzte Annotation darin enthalten ist.

Sicherheits-Checkliste

  • TLS am Proxy mit einem gültigen Zertifikat terminieren
  • secret_key oder POTATO_SECRET_KEY setzen, damit Sitzungen einen Neustart überstehen
  • admin_api_key setzen, statt die erzeugte Datei auszuliefern
  • Registrierung mit user_config.users einschränken
  • debug ausgeschaltet lassen, da der Debug-Modus die Anfragen des Reverse Proxy ohne Schlüssel zu den Admin-Seiten durchlässt
  • gunicorn mit einem einzigen Worker ausführen
  • Zugangsdaten in Umgebungsvariablen halten, nicht in der Konfiguration
  • POTATO_PROXY_FIX nur hinter einem vertrauenswürdigen Proxy aktivieren
  • Ausgabeverzeichnis, Kontendatei und Datenbanken sichern und eine Wiederherstellung testen
  • Überprüfen, wer eine Admin-Rolle hat

Umgebungsvariablen

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

Einen Signaturschlüssel erzeugen Sie mit:

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

In Docker Compose laden:

yaml
services:
  potato:
    env_file:
      - .env

Fehlerbehebung

Protokolle prüfen

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

Häufige Probleme

502 Bad Gateway: Prüfen Sie, ob Potato läuft und an den Port gebunden ist, an den der Proxy weiterleitet.

Nach einem Deployment sind alle abgemeldet: secret_key ist nicht gesetzt, daher wurde beim Start ein neuer Signaturschlüssel erzeugt.

Fehlende Annotationen: Prüfen Sie die Anzahl der Worker. Mehr als ein gunicorn-Worker verwirft Annotationen, ohne es zu melden.

Assets liefern unter einem Pfadpräfix 404: Setzen Sie POTATO_PROXY_FIX oder POTATO_URL_PREFIX, und prüfen Sie, ob der Proxy das Präfix vor dem Weiterleiten entfernt.

Langsam bei einem großen Korpus: Erweitern Sie den Speicher, bevor Sie Server hinzufügen, und ziehen Sie ab etwa einer Million Items item_store.backend: paged in Betracht.

Nächste Schritte

Details zur Implementierung finden Sie in der Anleitung zum Reverse Proxy und in der Anleitung zur Skalierung.