Zum Inhalt springen

Potato in Docker ausführen

Ein Potato-Projekt aus dem veröffentlichten Docker-Image bereitstellen, die Annotationen auf dem eigenen Datenträger behalten und den Admin-Schlüssel sowie die Umgebungsvariablen setzen, die der Container liest.

Diese Seite behandelt den Betrieb eines Potato-Annotationsprojekts in einem Container, vom ersten docker run bis zum Wechsel einer laufenden Studie auf ein neues Release. Sie richtet sich an Forschende und Laboradministratoren, die eine Studie von ihrem eigenen Rechner oder Server aus betreiben und Potato ohne Python-Installation auf dem Host nutzen möchten. Potato veröffentlicht das Image in der GitHub Container Registry als ghcr.io/davidjurgens/potato, und das Image enthält den Server und seine Abhängigkeiten und sonst nichts. Weil Ihr Projektordner beim Start des Containers unter /app eingebunden wird, bedient ein Image jede Aufgabe.

potato deploy führt dasselbe Image auf den Cloud-Hosts aus, die der Befehl einrichtet, und wählt dabei den Tag, der zu Ihrer installierten Potato-Version passt. Potato lokal oder in der Cloud ausführen behandelt diesen Weg, und Produktionseinrichtung behandelt gunicorn, einen Reverse Proxy und TLS auf einem Server, den Sie selbst verwalten.

Das Image beziehen

Die Installation der Abhängigkeiten numpy, pandas, scipy und scikit-learn von Potato mit pip geht auf einem Rechner mit 1 GB Speicher der Arbeitsspeicher aus und dauert auf einem mit 2 GB fünf bis zehn Minuten. Das Image liefert diese Abhängigkeiten bereits installiert mit:

bash
docker pull ghcr.io/davidjurgens/potato:2.14.0

Das Image gibt es in zwei Varianten, jeweils mit einem beweglichen Tag, einem Tag pro Release und einem Tag pro Commit:

TagInhalt
latest, 2.14.0die Kernabhängigkeiten von Potato, die Backup-Clients (huggingface_hub und boto3) und Authlib für SSO-Logins
latest-all, 2.14.0-allalles Obige sowie das Extra all mit den KI-SDKs hinter ai_support, PDF- und DOCX-Import, Parquet-Export und dem MySQL-Treiber
sha-<short>, sha-<short>-allein Commit des Master-Branches

Beide Varianten werden für linux/amd64 und linux/arm64 gebaut, und der arm64-Build läuft auf Apple Silicon nativ. Keine der Varianten enthält das Extra vision mit seinen torch-Wheels von mehreren Gigabyte. Die Segmentierung im Browser funktioniert auch ohne das Extra, weil ONNX Runtime Web im Potato-Paket mitgeliefert wird.

Legen Sie sich für jede Studie, die länger als einen Tag läuft, auf einen Versions-Tag fest. Der Tag latest wird bei jeder Änderung neu aus dem Master-Branch gebaut, und zwei docker pull-Aufrufe im Abstand einer Woche können unterschiedlichen Code laden. Ein Versions-Tag wie 2.14.0 wird einmal zum Release-Zeitpunkt gesetzt und danach nicht mehr neu gebaut.

Das Image selbst bauen

Bauen Sie aus einem Klon des Repositorys, wenn Sie ein Extra brauchen, das keine der veröffentlichten Varianten enthält. Das Build-Argument POTATO_EXTRAS akzeptiert jedes in der setup.py von Potato genannte Extra oder mehrere, durch Kommas getrennt:

bash
git clone https://github.com/davidjurgens/potato
cd potato
docker build -t potato .
docker build --build-arg POTATO_EXTRAS=all -t potato:all .
docker build --build-arg POTATO_EXTRAS=all,saml -t potato:all-saml .

Das Extra saml für die SAML-Anmeldung gehört nicht zu all. Das Dockerfile installiert die Bibliothek libxmlsec1, gegen die das Extra kompiliert wird, nur dann, wenn POTATO_EXTRAS saml nennt. Starten Sie einen lokalen Build über seinen Tag, etwa potato:all, überall dort, wo diese Seite ghcr.io/davidjurgens/potato:2.14.0 nennt.

Ein Projekt ausführen

Ein Projektordner enthält config.yaml und jede Datei, die die Konfiguration nennt. Die Beispiele auf dieser Seite verwenden eine Sentiment-Aufgabe mit drei Items und folgendem Aufbau:

text
myproject/
├── config.yaml
└── data/
    └── items.jsonl

Die Konfiguration hält jeden Pfad relativ zum Projektordner:

yaml
annotation_task_name: "Sentiment pilot"
task_dir: .
data_files:
  - data/items.jsonl
item_properties:
  id_key: id
  text_key: text
output_annotation_dir: annotation_output/
user_config:
  allow_all_users: true
annotation_schemes:
  - annotation_type: radio
    name: sentiment
    description: "What is the sentiment of this sentence?"
    labels: [positive, neutral, negative]

Jede Zeile von data/items.jsonl ist ein Item:

json
{"id": "1", "text": "The battery lasts all day."}
{"id": "2", "text": "The screen cracked within a week."}
{"id": "3", "text": "It arrived on Tuesday."}

Starten Sie den Server aus dem Ordner, der myproject enthält:

bash
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:2.14.0

Die Aufgabe ist dann unter http://localhost:8000 erreichbar. Das Argument -p 8000:7860 bildet Port 8000 Ihres Rechners auf Port 7860 ab, auf dem der Server im Container lauscht. Das Argument -v bindet den Projektordner unter /app ein. Die Host-Seite muss ein Pfad wie $PWD/myproject sein, weil Docker einen bloßen Namen wie myproject als Namen eines neuen, leeren Volumes liest. docker logs -f potato verfolgt das Serverprotokoll, und docker stop potato hält den Server an.

Ein Projekt mit mehreren Konfigurationen wählt mit POTATO_CONFIG eine aus, als Pfad relativ zu /app:

bash
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  -e POTATO_CONFIG=configs/pilot.yaml \
  ghcr.io/davidjurgens/potato:2.14.0

Eine Studie ausführen, die Wochen dauert

Eine Studie, die Wochen läuft, braucht einen Signierschlüssel für Sitzungen, der über Neustarts und Updates hinweg gleich bleibt. Ohne POTATO_SECRET_KEY wählt jeder Start des Servers einen neuen zufälligen Schlüssel und meldet alle Annotatoren ab. Legen Sie einmal eine Env-Datei außerhalb des Projektordners an und übergeben Sie dieselbe Datei an jedes docker run:

bash
printf 'POTATO_SECRET_KEY=%s\nPOTATO_ADMIN_API_KEY=%s\n' \
  "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > potato.env
chmod 600 potato.env
 
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  --env-file potato.env \
  --restart unless-stopped \
  ghcr.io/davidjurgens/potato:2.14.0

Der Abschnitt zum Admin-Schlüssel erklärt die zweite Zeile der Datei. --restart unless-stopped bringt den Container nach einem Absturz oder einem Neustart des Hosts zurück.

Weitere Befehle im Image

Alles, was nach dem Image-Namen steht, ersetzt den Server. Prüfen Sie eine Konfiguration mit dem Validator, den das Image mitbringt, oder öffnen Sie eine Shell im Container:

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

Dateibesitz unter Linux

Der Server im Image läuft als uid 1000, und ein Bind-Mount behält den Besitz, den der Ordner auf dem Host hat. Unter Linux gehört ein Ordner, den Sie angelegt haben, Ihrer eigenen uid, und ein von root angelegter Ordner gehört root. Ist der Besitzer des Ordners nicht uid 1000, kann der Server keine Annotationen in den Ordner schreiben. Der Container prüft den Ordner, bevor der Server startet, und bricht mit ERROR: /app is not writable by uid 1000 ab, statt mitten im Start zu scheitern. Jeder der folgenden Befehle behebt das Problem:

bash
sudo chown -R 1000:1000 myproject                  # hand the folder to the container user
docker run --user "$(id -u):$(id -g)" ...          # or run the container as yourself

Mit --user gehören die Annotationen und die Admin-Schlüsseldatei, die Potato schreibt, ebenfalls Ihrem eigenen Konto. Docker Desktop unter macOS und Windows ignoriert den Besitz bei Bind-Mounts, und ein Projekt, das auf einem Mac läuft, kann auf einem Linux-Server dennoch scheitern. POTATO_ALLOW_READONLY_APP=1 überspringt die Prüfung. Potato schreibt Annotationen, potato.log und seine SQLite-Datenbanken weiterhin in das Projekt, setzen Sie die Variable also nur, wenn diese Pfade über getrennte Mounts beschreibbar sind.

Manche Hosts starten jeden Container als root, was auch immer das Image vorgibt. Wenn der Container als root startet, übergibt er /app an den Benutzer potato des Images und startet den Server als dieser Benutzer. Setzen Sie POTATO_RUN_AS_ROOT=1, um stattdessen root beizubehalten.

Die Annotationen behalten

Potato schreibt alles, was eine Studie sammelt, in den Projektordner. Annotationen liegen unter output_annotation_dir, in einem Verzeichnis pro Annotator, das die user_state.json dieses Annotators enthält. Zwei SQLite-Datenbanken liegen neben config.yaml. project.sqlite enthält Memos, das Codebuch, Fälle, den Suchindex und den Review-Workflow, und datasets.sqlite enthält Evaluationsdatensätze und deren Versionen. Keine der beiden Datenbanken lässt sich aus den Datendateien neu aufbauen. Mit einem Bind-Mount liegen alle diese Dateien auf Ihrem Datenträger und bleiben dort, wenn der Container entfernt wird.

output_annotation_dir muss innerhalb des Projektordners aufgelöst werden. Ein Pfad wie /data/annotations bricht den Start des Servers mit der Meldung output_annotation_dir: Path '/data/annotations' resolves to '/data/annotations' which is outside the project directory '/app' ab. Behalten Sie einen relativen Pfad wie annotation_output/ bei. Das /data-Volume des Images liegt ebenfalls außerhalb des Projekts, und Potato schreibt dort nichts.

Kopieren Sie eine Datenbank aus einem laufenden Container mit dem Backup-Befehl von SQLite, niemals als einfache Datei. project.sqlite läuft im WAL-Modus, während der Server in die Datenbank schreibt, und eine einfache Kopie kann beschädigt sein oder neuere Arbeit ohne jede Warnung auslassen. Das Image enthält das Werkzeug sqlite3 für den Snapshot:

bash
docker exec potato sqlite3 /app/project.sqlite ".backup /tmp/snap.sqlite"
docker cp potato:/tmp/snap.sqlite ./project.sqlite

Die meisten verwalteten Container-Hosts löschen das Dateisystem des Containers, wenn der Container neu startet. Halten Sie das Projekt auf einem solchen Host auf persistentem Speicher, der unter /app eingebunden ist, oder fügen Sie der Konfiguration einen backup-Block hinzu. Der Block kopiert die Annotationen und Datenbank-Snapshots nach Zeitplan in einen S3-Bucket oder ein Hugging-Face-Dataset und stellt sie wieder her, wenn der Server mit leerem Datenträger startet. Jede Variante des Images enthält die Clients, die der Block braucht.

Umgebungsvariablen

Der Container liest diese Variablen beim Start:

VariableStandardZweck
POTATO_CONFIGconfig.yamlPfad der Konfiguration im Container, relativ zu /app
PORT7860Port, auf dem der Server im Container lauscht
GUNICORN_WORKERS1Worker-Prozesse; jeder andere Wert bricht den Start des Containers ab
GUNICORN_THREADS8Threads im einen Worker und die Zahl der gleichzeitig bedienten Anfragen
GUNICORN_TIMEOUT120Sekunden, die ein Worker stumm bleiben darf, bevor gunicorn ihn neu startet
POTATO_SECRET_KEYbei jedem Start zufälligSigniert Sitzungs-Cookies; secret_key in der Konfiguration hat Vorrang
POTATO_ADMIN_API_KEYgenerierte DateiDer Admin-API-Schlüssel; admin_api_key in der Konfiguration hat Vorrang
POTATO_GENERATED_TEMPLATES_DIRbeim Start gewähltWohin Potato die beim Start erzeugten Aufgaben-Templates schreibt
POTATO_BUNDLE_URLnicht gesetztURL eines .tar.gz des Projekts, das beim Start nach /app geladen wird
POTATO_BUNDLE_SHA256nicht gesetztPrüfsumme, die das heruntergeladene Projekt erfüllen muss
POTATO_BUNDLE_TOKENnicht gesetztWird mit dem Download als Authorization: Bearer gesendet
POTATO_RUN_AS_ROOTnicht gesetzt1 lässt den Server als root weiterlaufen, wenn der Container als root startet
POTATO_ALLOW_MULTIWORKERnicht gesetzt1 erlaubt, dass GUNICORN_WORKERS 1 übersteigt, was in einer echten Studie Annotationen verliert
POTATO_ALLOW_READONLY_APPnicht gesetzt1 überspringt die Startprüfung, ob /app beschreibbar ist

Ein Container, der PORT ändert, braucht denselben Port rechts von -p, etwa -p 8000:9000 für PORT=9000.

Anzahl der Worker und Threads

Der Container weigert sich, mit mehr als einem gunicorn-Worker zu starten. Potato hält seinen Item-Pool, seine Zuweisungswarteschlange und den Zustand jedes Annotators im Speicher eines Prozesses. Ein zweiter Worker hätte von allen dreien eine eigene Kopie und würde Items zuweisen, die der erste Worker bereits zugewiesen hat. Jedes Speichern schreibt user_state.json vollständig neu, und der Worker, der zuletzt speichert, würde die Annotationen des anderen ohne Fehlermeldung verwerfen. Erhöhen Sie stattdessen GUNICORN_THREADS für mehr gleichzeitige Anfragen, denn Threads teilen sich eine Kopie des Zustands.

Generierte Templates

Potato baut beim Start für die Annotationsseite jeder Aufgabe ein Template. Außerhalb eines Containers landet das Template im installierten Paket, sofern das Paketverzeichnis beschreibbar ist. Das Image installiert Potato als root und führt den Server als uid 1000 aus, sodass der Server nicht in das Paketverzeichnis schreiben kann. Potato schreibt die Templates stattdessen in ein Verzeichnis unter /tmp, das nur der Benutzer des Servers lesen oder beschreiben kann, und das Startprotokoll nennt das Verzeichnis. Potato baut die Templates bei jedem Start aus der Konfiguration neu, und der Verlust des Verzeichnisses kostet nichts. Setzen Sie POTATO_GENERATED_TEMPLATES_DIR, um die Templates an einem anderen Ort abzulegen, etwa auf einem tmpfs-Mount.

Das Projekt beim Start herunterladen

Hosts, die nur ein Image ausführen, etwa Render, Fly, Railway und ECS, haben keinen Ordner zum Einbinden. Ist POTATO_BUNDLE_URL gesetzt, lädt der Container ein .tar.gz des Projekts herunter, prüft den Download gegen POTATO_BUNDLE_SHA256 und entpackt das Projekt nach /app, bevor der Server startet. Der Container schreibt die Prüfsumme nach /app/.potato-bundle-sha, und ein Neustart auf einem persistenten Datenträger überspringt einen Download, dessen Prüfsumme übereinstimmt. In einem von potato deploy gebauten Bundle ersetzt eine neue Version des Projekts nie die Annotationsausgabe oder die SQLite-Datenbanken, die bereits auf dem Datenträger liegen. potato deploy up setzt diese Variablen für Sie.

Der Admin-Schlüssel in einem Container

Das Admin-Dashboard unter /admin und die Admin-API verwenden einen gemeinsamen Schlüssel, der als Header X-API-Key gesendet oder auf der Admin-Anmeldeseite eingegeben wird. Potato liest den Schlüssel zuerst aus dem Top-Level-Konfigurationsschlüssel admin_api_key, dann aus der Umgebungsvariablen POTATO_ADMIN_API_KEY. In einem Container hält die Umgebungsvariable den Schlüssel aus config.yaml und aus jedem Repository heraus, das das Projekt enthält. Die Datei potato.env aus dem Abschnitt zu langen Studien setzt die Variable, und der Admin-Schlüssel ist dann das, was diese Datei enthält:

bash
grep POTATO_ADMIN_API_KEY potato.env

Setzen weder die Konfiguration noch die Umgebung einen Schlüssel, erzeugt Potato einen Schlüssel, sobald jemand zum ersten Mal /admin öffnet oder einen Admin-Endpunkt aufruft. Potato schreibt den erzeugten Schlüssel in admin_api_key.txt im Aufgabenverzeichnis. Im Container ist das Aufgabenverzeichnis /app, der eingebundene Projektordner. Die Datei ist nur für den Benutzer des Servers lesbar, und Potato liest nach einem Neustart dieselbe Datei. Das Serverprotokoll nennt die Datei und gibt den Schlüssel nie aus. Lesen Sie den Schlüssel über den Container:

bash
docker exec potato cat /app/admin_api_key.txt

Auf einem Linux-Host, auf dem Sie den Projektordner an uid 1000 übergeben haben, kann Ihr eigenes Konto admin_api_key.txt nicht ohne sudo lesen. Ein Container, der mit --user "$(id -u):$(id -g)" gestartet wird, schreibt die Datei stattdessen als Ihr eigenes Konto. Welche Bereiche der Schlüssel öffnet, steht auf der Seite Admin-Dashboard.

Debug-Modus in einem Container

Lassen Sie debug: false in jedem Container, der eine Studie bedient. Mit debug: true in der Konfiguration ändert Potato das Verhalten von zwei Funktionen:

  • Admin-Seiten ohne Schlüssel. Das Admin-Dashboard und die Admin-API verzichten auf den Schlüssel bei einer Anfrage von einer Loopback-Adresse, die keinen Header X-Forwarded-For, X-Real-IP oder Forwarded trägt. Im Standard-Bridge-Netzwerk von Docker erreicht eine Anfrage an den veröffentlichten Port den Container von der Adresse der Bridge aus und braucht weiterhin den Schlüssel. Mit --network host ist eine Anfrage vom Host-Rechner eine Loopback-Anfrage und braucht keinen Schlüssel. Ein Reverse Proxy im selben Netzwerk-Namespace, der keinen Weiterleitungs-Header hinzufügt, lässt jede Anfrage, die der Proxy weiterreicht, lokal aussehen.
  • Gemeinsames Debug-Konto. Ist zusätzlich debug_phase gesetzt, wird jeder Besucher als ein gemeinsames Konto, debug_user, angemeldet und in diese Phase übersprungen. Das Konto ist kein Admin, aber alle Besucher teilen sich die Antworten des Kontos.

Der Server protokolliert beim Start eine Warnung, sobald debug aktiv ist. Ein mit docker exec im Container ausgeführter Befehl ist eine Loopback-Anfrage. Im Debug-Modus liefert docker exec potato curl -s http://127.0.0.1:7860/admin/health daher den Admin-Gesundheitsbericht ohne Schlüssel.

Health-Checks

GET /health braucht keine Authentifizierung und meldet nur, ob der Server läuft. Die Route liefert {"status": "ok"}, sobald der Server bereit ist, und den Status 503, solange der Server noch Daten lädt, und ein Load Balancer oder ein Orchestrator kann die Route als Bereitschaftsprüfung abfragen. Der HEALTHCHECK des Images fragt /health alle 30 Sekunden ab und gewährt für große Aufgaben eine Startphase von 90 Sekunden, und docker ps zeigt das Ergebnis an. GET /admin/health meldet den Namen der Aufgabe und den Zustand der Item- und Annotator-Manager, und die Route braucht den Admin-Schlüssel im Header X-API-Key.

Docker Compose

Das Potato-Repository liefert keine Compose-Datei mit. Produktionseinrichtung zeigt eine Compose-Datei, die dieses Image hinter nginx ausführt, mit dem unter /app eingebundenen Projektordner und den aus der Umgebung gelesenen Sitzungs- und Admin-Schlüsseln.

Auf eine neue Version aktualisieren

Ein Update von Potato ersetzt den Container und lässt den Projektordner mit allen Annotationen und Datenbanken an Ort und Stelle. Ziehen Sie vor dem Wechsel das neue Release, prüfen Sie die Konfiguration mit dem Validator des neuen Releases und erstellen Sie einen Snapshot der Projektdatenbank. Ersetzen Sie 2.14.0 durch das Release, auf das Sie wechseln:

bash
docker pull ghcr.io/davidjurgens/potato:2.14.0
docker run --rm -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:2.14.0 potato validate config.yaml --strict
docker exec potato sqlite3 /app/project.sqlite ".backup /tmp/snap.sqlite"
docker cp potato:/tmp/snap.sqlite ./project-before-update.sqlite

Ersetzen Sie dann den laufenden Container durch einen Container mit dem neuen Tag, mit demselben Mount und derselben Env-Datei:

bash
docker stop potato
docker rm potato
docker run -d --name potato -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  --env-file potato.env \
  --restart unless-stopped \
  ghcr.io/davidjurgens/potato:2.14.0

Weil potato.env POTATO_SECRET_KEY unverändert hält, bleiben die Annotatoren über den Wechsel hinweg angemeldet, und der Admin-Schlüssel ändert sich nicht.

Fehlerbehebung

ERROR: config file not found: config.yaml. Das Projekt wurde nicht dort eingebunden, wo der Server suchte. Der Fehler listet zum Vergleich mit Ihrem -v-Argument den Inhalt von /app auf. Eine leere Liste deutet meist auf einen bloßen Namen wie -v myproject:/app hin. Docker liest einen bloßen Namen als neues, leeres Volume und nicht als Ihren Ordner.

ERROR: /app is not writable by uid 1000. Der Server kann nicht in den Projektordner schreiben. Übergeben Sie den Ordner an uid 1000 oder führen Sie den Container mit --user aus. Dateibesitz unter Linux nennt beide Befehle.

output_annotation_dir ... is outside the project directory '/app'. Die Konfiguration legt die Annotationsausgabe außerhalb des eingebundenen Ordners ab. Verwenden Sie einen relativen Pfad wie annotation_output/.

Nach einem Neustart sind alle abgemeldet. Es ist kein Signierschlüssel gesetzt, und der Server hat beim Start einen neuen zufälligen Schlüssel gewählt. Setzen Sie POTATO_SECRET_KEY in einer Env-Datei und übergeben Sie die Datei an jedes docker run.

Der Container bricht mit einem Fehler ab, der GUNICORN_WORKERS nennt. Die Worker-Anzahl liegt über 1. Setzen Sie GUNICORN_WORKERS=1 oder entfernen Sie die Variable, und erhöhen Sie stattdessen GUNICORN_THREADS.

Die Admin-Anmeldeseite weist jeden Schlüssel ab. admin_api_key aus der Konfiguration hat Vorrang vor POTATO_ADMIN_API_KEY, und beide haben Vorrang vor admin_api_key.txt. Prüfen Sie zuerst die Konfiguration, dann docker exec potato printenv POTATO_ADMIN_API_KEY.

Weiterführende Literatur

Implementierungsdetails finden Sie in der Quelldokumentation.