コンテンツへスキップ

Docker で Potato を実行する

公開されている Docker イメージから Potato プロジェクトを配信し、アノテーションを自分のディスクに残し、コンテナが読み取る管理者キーと環境変数を設定します。

このページでは、最初の docker run から、運用中の研究を新しいリリースへ移行するところまで、Potato のアノテーションプロジェクトをコンテナで実行する方法を説明します。対象は、自分のマシンやサーバーで研究を配信していて、ホストに Python をインストールせずに Potato を使いたい研究者とラボの管理者です。Potato はイメージを GitHub Container Registry に ghcr.io/davidjurgens/potato として公開しています。イメージにはサーバーと依存パッケージだけが入っています。コンテナの起動時にプロジェクトフォルダーが /app にマウントされるため、1つのイメージですべてのタスクを配信できます。

potato deploy は、コマンドが設定するクラウドホスト上で同じイメージを実行し、インストール済みの Potato のバージョンに対応するタグを選びます。その経路は Potato をローカルまたはクラウドで実行する で、自分で管理するサーバー上の gunicorn、リバースプロキシ、TLS は 本番セットアップ で扱います。

イメージの入手

Potato の依存パッケージである numpy、pandas、scipy、scikit-learn を pip でインストールすると、メモリ 1 GB のマシンでは容量不足になり、2 GB のマシンでも5分から10分かかります。イメージには、これらの依存パッケージがインストール済みの状態で入っています。

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

イメージには2つのバリアントがあり、それぞれに最新を指すタグ、リリースごとのタグ、コミットごとのタグがあります。

タグ内容
latest、2.14.0Potato のコア依存パッケージ、バックアップ用クライアント(huggingface_hub と boto3)、SSO ログイン用の Authlib
latest-all、2.14.0-all上記すべてに加えて、ai_support の背後にある AI SDK、PDF と DOCX の取り込み、parquet エクスポート、MySQL ドライバーを含む all エクストラ
sha-<short>、sha-<short>-allmaster ブランチの1つのコミット

どちらのバリアントも linux/amd64 と linux/arm64 向けにビルドされており、arm64 版は Apple Silicon でネイティブに動作します。どちらのバリアントにも、数ギガバイトの torch ホイールを含む vision エクストラは入っていません。ブラウザー側のセグメンテーションは、ONNX Runtime Web が Potato パッケージに同梱されているため、このエクストラなしで動作します。

1日を超えて続く研究では、バージョンタグを固定してください。latest タグは master ブランチが変わるたびに再ビルドされるため、1週間空けた2回の docker pull で異なるコードを取得することがあります。2.14.0 のようなバージョンタグはリリース時に一度だけプッシュされ、その後に再ビルドされることはありません。

イメージを自分でビルドする

公開されている2つのバリアントのどちらにも含まれないエクストラが必要な場合は、リポジトリのクローンからビルドします。POTATO_EXTRAS ビルド引数には、Potato の setup.py に載っているエクストラを1つ、またはカンマ区切りで複数指定できます。

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 .

SAML サインイン用の saml エクストラは all に含まれません。このエクストラがコンパイル時に使う libxmlsec1 ライブラリを、Dockerfile は POTATO_EXTRAS に saml がある場合にだけインストールします。ローカルでビルドしたイメージは、このページで ghcr.io/davidjurgens/potato:2.14.0 と書かれている箇所で、potato:all のようなタグを指定して実行してください。

プロジェクトの実行

プロジェクトフォルダーには、config.yaml と、その設定が指定するすべてのファイルが入っています。このページの例では、次の構成の3項目からなる感情分析タスクを使います。

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

設定では、すべてのパスをプロジェクトフォルダーからの相対パスにしています。

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]

data/items.jsonl の各行が1つの項目です。

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."}

myproject を含むフォルダーからサーバーを起動します。

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

タスクは http://localhost:8000 で開けます。-p 8000:7860 引数は、お使いのマシンのポート 8000 を、コンテナ内でサーバーが待ち受けるポート 7860 に対応づけます。-v 引数はプロジェクトフォルダーを /app にマウントします。ホスト側は $PWD/myproject のようなパスにする必要があります。myproject のような名前だけを指定すると、Docker はそれを新しい空のボリュームの名前として扱うためです。docker logs -f potato はサーバーログを追跡し、docker stop potato はサーバーを停止します。

複数の設定ファイルがあるプロジェクトでは、/app からの相対パスを POTATO_CONFIG に指定して1つを選びます。

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

数週間続く研究を実行する

数週間続く研究では、再起動や更新をまたいで変わらないセッション署名キーが必要です。POTATO_SECRET_KEY がないと、サーバーは起動のたびに新しいランダムなキーを選び、すべてのアノテーターをサインアウトさせます。環境変数ファイルをプロジェクトフォルダーの外に一度だけ作成し、同じファイルをすべての 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

ファイルの2行目については、管理者キーのセクションで説明します。--restart unless-stopped は、クラッシュやホストの再起動の後にコンテナを復帰させます。

イメージ内のその他のコマンド

イメージ名の後ろに書いたものは、サーバーの代わりに実行されます。イメージに入っているバリデーターで設定を確認することも、コンテナ内でシェルを開くこともできます。

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

Linux でのファイル所有権

イメージ内のサーバーは uid 1000 で動作し、バインドマウントはホスト上でフォルダーが持つ所有権をそのまま保ちます。Linux では、自分で作成したフォルダーは自分の uid の所有になり、root が作成したフォルダーは root の所有になります。フォルダーの所有者が uid 1000 でない限り、サーバーはそのフォルダーにアノテーションを書き込めません。コンテナはサーバーを起動する前にフォルダーを確認し、起動の途中で失敗する代わりに ERROR: /app is not writable by uid 1000 を出して停止します。次のどちらのコマンドでも解決できます。

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

--user を使うと、Potato が書き込むアノテーションと管理者キーファイルも自分のアカウントの所有になります。macOS と Windows の Docker Desktop はバインドマウントの所有権を無視するため、Mac で動いたプロジェクトが Linux サーバーでは失敗することがあります。POTATO_ALLOW_READONLY_APP=1 はこの確認を省略します。Potato はアノテーション、potato.log、SQLite データベースを引き続きプロジェクト内に書き込むため、これらのパスが別のマウントで書き込み可能な場合にのみ、この変数を設定してください。

ホストによっては、イメージの指定にかかわらず、すべてのコンテナを root で起動します。コンテナが root で起動した場合、コンテナは /app をイメージの potato ユーザーに引き渡し、そのユーザーとしてサーバーを起動します。root のままにするには POTATO_RUN_AS_ROOT=1 を設定してください。

アノテーションの保存

Potato は、研究で集まったものをすべてプロジェクトフォルダーに書き込みます。アノテーションは output_annotation_dir の下に、アノテーターごとに1つのディレクトリとして保存され、そこにそのアノテーターの user_state.json が入ります。config.yaml の隣には2つの SQLite データベースがあります。project.sqlite にはメモ、コードブック、ケース、検索インデックス、レビューワークフローが入り、datasets.sqlite には評価データセットとそのバージョンが入ります。どちらのデータベースも、データファイルから再構築することはできません。バインドマウントを使えば、これらのファイルはすべて自分のディスク上にあり、コンテナを削除しても残ります。

output_annotation_dir は、プロジェクトフォルダーの内側に解決される必要があります。/data/annotations のようなパスを指定すると、サーバーは起動時に output_annotation_dir: Path '/data/annotations' resolves to '/data/annotations' which is outside the project directory '/app' というメッセージを出して停止します。annotation_output/ のような相対パスにしてください。イメージの /data ボリュームもプロジェクトの外にあり、Potato はそこに何も書き込みません。

実行中のコンテナからデータベースをコピーするときは、通常のファイルとしてではなく、SQLite のバックアップコマンドを使ってください。サーバーがデータベースに書き込んでいる間、project.sqlite は WAL モードで動作しており、通常のコピーは壊れていたり、最近の作業が抜けていたりしても、何の警告も出ません。イメージにはスナップショット用の sqlite3 ツールが入っています。

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

多くのマネージド型コンテナホストは、コンテナの再起動時にコンテナのファイルシステムを消去します。そのようなホストでは、/app にマウントした永続ストレージにプロジェクトを置くか、設定に backup ブロックを追加してください。このブロックは、アノテーションとデータベースのスナップショットを、S3 バケットまたは Hugging Face データセットに定期的にコピーし、サーバーが空のディスクで起動したときに復元します。このブロックに必要なクライアントは、イメージのどのバリアントにも入っています。

環境変数

コンテナは起動時に次の変数を読み取ります。

変数デフォルト用途
POTATO_CONFIGconfig.yamlコンテナ内の設定ファイルのパス(/app からの相対パス)
PORT7860コンテナ内でサーバーが待ち受けるポート
GUNICORN_WORKERS1ワーカープロセス数。これ以外の値ではコンテナが起動時に停止します
GUNICORN_THREADS81つのワーカー内のスレッド数。同時に処理できるリクエスト数になります
GUNICORN_TIMEOUT120ワーカーが無応答のままでいられる秒数。この秒数を超えると gunicorn がワーカーを再起動します
POTATO_SECRET_KEY起動のたびにランダムセッション Cookie に署名します。設定の secret_key が優先されます
POTATO_ADMIN_API_KEY生成されるファイル管理者 API キー。設定の admin_api_key が優先されます
POTATO_GENERATED_TEMPLATES_DIR起動時に決定Potato が起動時に作成するタスクテンプレートの書き込み先
POTATO_BUNDLE_URL未設定起動時に /app へダウンロードする、プロジェクトの .tar.gz の URL
POTATO_BUNDLE_SHA256未設定ダウンロードしたプロジェクトが一致しなければならないチェックサム
POTATO_BUNDLE_TOKEN未設定ダウンロード時に Authorization: Bearer として送信されます
POTATO_RUN_AS_ROOT未設定1 にすると、コンテナが root で起動した場合にサーバーを root のまま実行します
POTATO_ALLOW_MULTIWORKER未設定1 にすると GUNICORN_WORKERS を 1 より大きくできますが、実際の研究ではアノテーションが失われます
POTATO_ALLOW_READONLY_APP未設定1 にすると、/app が書き込み可能かどうかの起動時の確認を省略します

PORT を変更するコンテナでは、-p の右側にも同じポートが必要です。PORT=9000 なら -p 8000:9000 のようにします。

ワーカー数とスレッド数

コンテナは、gunicorn のワーカーが2つ以上ある状態では起動しません。Potato は、項目プール、割り当てキュー、すべてのアノテーターの状態を、1つのプロセスのメモリ内に保持しています。2つ目のワーカーは、この3つのコピーを別々に持ち、最初のワーカーがすでに割り当てた項目をもう一度割り当ててしまいます。保存のたびに user_state.json が丸ごと書き換えられるため、最後に保存したワーカーが、もう一方のワーカーのアノテーションを、エラーなしで破棄してしまいます。同時に処理するリクエストを増やすには、代わりに GUNICORN_THREADS を増やしてください。スレッドは状態のコピーを1つだけ共有します。

生成されるテンプレート

Potato は、起動時に各タスクのアノテーションページ用のテンプレートを作成します。コンテナの外では、パッケージのディレクトリが書き込み可能であれば、テンプレートはインストール済みのパッケージに書き込まれます。イメージは Potato を root としてインストールし、サーバーを uid 1000 で実行するため、サーバーはパッケージのディレクトリに書き込めません。そのため Potato は、サーバーのユーザーだけが読み書きできる /tmp 以下のディレクトリにテンプレートを書き込み、そのディレクトリ名を起動ログに出力します。Potato は起動のたびに設定からテンプレートを再作成するため、このディレクトリが失われても損失はありません。テンプレートを別の場所(tmpfs マウントなど)に置くには、POTATO_GENERATED_TEMPLATES_DIR を設定します。

起動時にプロジェクトをダウンロードする

Render、Fly、Railway、ECS のように、イメージを実行するだけのホストには、マウントするフォルダーがありません。POTATO_BUNDLE_URL を設定すると、コンテナはプロジェクトの .tar.gz をダウンロードし、ダウンロードしたものを POTATO_BUNDLE_SHA256 と照合して、サーバーの起動前にプロジェクトを /app に展開します。コンテナはチェックサムを /app/.potato-bundle-sha に書き込み、永続ディスク上での再起動では、チェックサムが一致するダウンロードを省略します。potato deploy が作成したバンドルでは、プロジェクトの新しいバージョンが、ディスク上にあるアノテーション出力や SQLite データベースを置き換えることはありません。potato deploy up はこれらの変数を自動で設定します。

コンテナ内の管理者キー

/admin の管理ダッシュボードと管理 API は、共通の1つのキーを受け付けます。キーは X-API-Key ヘッダーとして送るか、管理者ログインページに入力します。Potato はまず最上位の admin_api_key 設定キーからキーを読み取り、次に POTATO_ADMIN_API_KEY 環境変数から読み取ります。コンテナでは、環境変数を使うことで、キーが config.yaml にも、プロジェクトを保管するリポジトリにも入らずに済みます。長期の研究のセクションにある potato.env ファイルがこの変数を設定するため、管理者キーはそのファイルに書かれた値になります。

bash
grep POTATO_ADMIN_API_KEY potato.env

設定にも環境変数にもキーがない場合、Potato は、誰かが初めて /admin を開くか管理者エンドポイントを呼び出したときに、キーを生成します。Potato は生成したキーをタスクディレクトリの admin_api_key.txt に書き込みます。コンテナでは、タスクディレクトリはマウントしたプロジェクトフォルダーである /app です。このファイルはサーバーのユーザーだけが読み取れ、Potato は再起動の後も同じファイルを読み取ります。サーバーログにはファイル名が出力されますが、キーそのものは出力されません。キーはコンテナ経由で読み取ります。

bash
docker exec potato cat /app/admin_api_key.txt

プロジェクトフォルダーを uid 1000 に引き渡した Linux ホストでは、自分のアカウントは sudo なしでは admin_api_key.txt を読み取れません。--user "$(id -u):$(id -g)" で起動したコンテナは、このファイルを自分のアカウントとして書き込みます。キーで開けるものについては、管理ダッシュボード のページを参照してください。

コンテナ内のデバッグモード

研究を配信するコンテナでは、debug: false のままにしてください。設定で debug: true にすると、Potato は2つの機能の動作を変えます。

  • キーなしの管理者ページ。 管理ダッシュボードと管理 API は、X-Forwarded-For、X-Real-IP、Forwarded のいずれのヘッダーも持たないループバックアドレスからのリクエストに対して、キーを省略します。Docker のデフォルトのブリッジネットワークでは、公開ポートへのリクエストはブリッジのアドレスからコンテナに届くため、引き続きキーが必要です。--network host では、ホストマシンからのリクエストはループバックリクエストになり、キーは不要です。転送ヘッダーを付加しない、同じネットワーク名前空間内のリバースプロキシは、プロキシが渡すすべてのリクエストをローカルからのものに見せてしまいます。
  • 共有のデバッグアカウント。 debug_phase も設定されている場合、すべての訪問者が共有アカウントの1つである debug_user としてサインインし、そのフェーズに進まされます。このアカウントは管理者ではありませんが、すべての訪問者がこのアカウントの回答を共有します。

debug がオンの間は、サーバーが起動時に警告をログに出力します。docker exec でコンテナ内から実行したコマンドは、ループバックリクエストです。そのため、デバッグモードでは docker exec potato curl -s http://127.0.0.1:7860/admin/health が、キーなしで管理者ヘルスレポートを返します。

ヘルスチェック

GET /health は認証が不要で、サーバーが起動しているかどうかだけを返します。このルートは、サーバーの準備ができると {"status": "ok"} を返し、サーバーがまだデータを読み込んでいる間はステータス 503 を返します。ロードバランサーやオーケストレーターは、このルートを準備完了のチェックとしてポーリングできます。イメージ自身の HEALTHCHECK は /health を30秒ごとにポーリングし、大きなタスクのために90秒の起動猶予期間を設けており、結果は docker ps に表示されます。GET /admin/health はタスク名と、項目マネージャーおよびアノテーターマネージャーの状態を返し、このルートには X-API-Key ヘッダーで管理者キーを渡す必要があります。

Docker Compose

Potato のリポジトリには Compose ファイルが含まれていません。本番セットアップ には、このイメージを nginx の背後で実行する Compose ファイルが載っています。プロジェクトフォルダーは /app にマウントされ、セッションキーと管理者キーは環境変数から読み取られます。

新しいバージョンへの更新

Potato を更新すると、コンテナは置き換わりますが、すべてのアノテーションとデータベースを含むプロジェクトフォルダーはそのまま残ります。切り替える前に、新しいリリースを pull し、新しいリリースのバリデーターで設定を確認し、プロジェクトデータベースのスナップショットを取ってください。2.14.0 は、移行先のリリースに置き換えてください。

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

その後、同じマウントと同じ環境変数ファイルを使って、実行中のコンテナを新しいタグのコンテナに置き換えます。

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

potato.env の POTATO_SECRET_KEY は変わらないため、アノテーターは切り替えの前後でサインインしたままになり、管理者キーも変わりません。

トラブルシューティング

ERROR: config file not found: config.yaml。 プロジェクトが、サーバーが探す場所にマウントされていません。エラーには /app の内容が一覧で出るので、-v 引数と見比べてください。一覧が空の場合、多くは -v myproject:/app のように名前だけを指定したことが原因です。Docker は名前だけの指定を、自分のフォルダーではなく新しい空のボリュームとして扱います。

ERROR: /app is not writable by uid 1000。 サーバーがプロジェクトフォルダーに書き込めません。フォルダーを uid 1000 に引き渡すか、--user を付けてコンテナを実行してください。どちらのコマンドも Linux でのファイル所有権 にあります。

output_annotation_dir ... is outside the project directory '/app'。 設定が、アノテーションの出力先をマウントしたフォルダーの外に指定しています。annotation_output/ のような相対パスを使ってください。

再起動の後、全員がサインアウトされる。 署名キーが設定されておらず、サーバーが起動時に新しいランダムなキーを選びました。環境変数ファイルに POTATO_SECRET_KEY を設定し、そのファイルをすべての docker run に渡してください。

コンテナが GUNICORN_WORKERS に言及するエラーで終了する。 ワーカー数が 1 を超えています。GUNICORN_WORKERS=1 を設定するか、この変数を削除し、代わりに GUNICORN_THREADS を増やしてください。

管理者ログインページがどのキーも拒否する。 設定の admin_api_key は POTATO_ADMIN_API_KEY より優先され、その両方が admin_api_key.txt より優先されます。まず設定を確認し、次に docker exec potato printenv POTATO_ADMIN_API_KEY を確認してください。

関連資料

実装の詳細については、ソースドキュメントを参照してください。