Запуск Potato в Docker
Запустите проект Potato из опубликованного Docker-образа, храните разметку на своём диске и задайте ключ администратора и переменные окружения, которые читает контейнер.
На этой странице описан запуск проекта разметки Potato в контейнере, от первой команды docker run до переноса работающего исследования на новый релиз. Страница написана для исследователей и администраторов лабораторий, которые ведут исследование со своей машины или сервера и хотят получить Potato без установки Python на хосте. Potato публикует образ в GitHub Container Registry как ghcr.io/davidjurgens/potato, и в образе лежат сервер и его зависимости, больше ничего. Папка вашего проекта монтируется в /app при старте контейнера, поэтому один образ подходит для любой задачи.
potato deploy запускает тот же образ на облачных хостах, которые команда настраивает сама, и выбирает тег, соответствующий установленной у вас версии Potato. Этот путь описан на странице Запуск Potato локально или в облаке, а gunicorn, обратный прокси и TLS на сервере, которым вы управляете сами, описаны на странице Production-настройка.
Получение образа
Установка зависимостей Potato (numpy, pandas, scipy и scikit-learn) через pip упирается в нехватку памяти на машине с 1 GB и занимает от пяти до десяти минут на машине с 2 GB. В образе эти зависимости уже установлены:
docker pull ghcr.io/davidjurgens/potato:2.14.0Образ выходит в двух вариантах, и у каждого есть плавающий тег, тег на каждый релиз и тег на каждый коммит:
| Тег | Содержимое |
|---|---|
latest, 2.14.0 | Основные зависимости Potato, клиенты резервного копирования (huggingface_hub и boto3) и Authlib для входа через SSO |
latest-all, 2.14.0-all | Всё перечисленное выше и дополнение all с SDK для ИИ, на которых работает ai_support, приёмом PDF и DOCX, экспортом в parquet и драйвером MySQL |
sha-<short>, sha-<short>-all | Один коммит ветки master |
Оба варианта собираются для linux/amd64 и linux/arm64, и сборка arm64 работает на Apple Silicon нативно. Ни один вариант не включает дополнение vision с его многогигабайтными колёсами torch. Сегментация в браузере работает и без этого дополнения, потому что ONNX Runtime Web входит в пакет Potato.
Для любого исследования, которое продлится дольше суток, закрепите тег версии. Тег latest пересобирается из ветки master после каждого изменения, и две команды docker pull с интервалом в неделю могут получить разный код. Тег версии, например 2.14.0, публикуется один раз при релизе и после этого не пересобирается.
Сборка образа самостоятельно
Собирайте образ из клона репозитория, когда вам нужно дополнение, которого нет ни в одном из опубликованных вариантов. Аргумент сборки POTATO_EXTRAS принимает любое дополнение, названное в setup.py Potato, или несколько через запятую:
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 не входит. Dockerfile устанавливает библиотеку libxmlsec1, с которой компилируется это дополнение, только когда POTATO_EXTRAS содержит saml. Локально собранный образ запускайте по его тегу, например potato:all, везде, где на этой странице указан ghcr.io/davidjurgens/potato:2.14.0.
Запуск проекта
Папка проекта содержит config.yaml и все файлы, которые называет конфигурация. В примерах на этой странице используется задача разметки тональности из трёх объектов со следующей структурой:
myproject/
├── config.yaml
└── data/
└── items.jsonlВсе пути в конфигурации заданы относительно папки проекта:
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 задаёт один объект:
{"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:
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, потому что Docker воспринимает голое имя вроде myproject как имя нового пустого тома. docker logs -f potato показывает журнал сервера по мере его пополнения, а docker stop potato останавливает сервер.
Если в проекте несколько конфигураций, нужную выбирает POTATO_CONFIG, путь относительно /app:
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:
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Вторая строка файла объяснена в разделе про ключ администратора. --restart unless-stopped возвращает контейнер после сбоя или перезагрузки хоста.
Другие команды в образе
Всё, что стоит после имени образа, заменяет запуск сервера. Проверьте конфигурацию валидатором из образа или откройте оболочку внутри контейнера:
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, а не ломается посреди запуска. Проблему решает любая из команд ниже:
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, тоже принадлежат вашей учётной записи. Docker Desktop в macOS и Windows игнорирует владельца на смонтированных папках, поэтому проект, работающий на 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, в отдельную папку на каждого разметчика с его файлом user_state.json. Рядом с config.yaml лежат две базы данных 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:
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_CONFIG | config.yaml | Путь к конфигурации внутри контейнера относительно /app |
PORT | 7860 | Порт, который сервер слушает внутри контейнера |
GUNICORN_WORKERS | 1 | Число рабочих процессов; любое другое значение останавливает контейнер при запуске |
GUNICORN_THREADS | 8 | Число потоков в единственном рабочем процессе и число запросов, обслуживаемых одновременно |
GUNICORN_TIMEOUT | 120 | Сколько секунд рабочий процесс может молчать, прежде чем gunicorn его перезапустит |
POTATO_SECRET_KEY | случайный при каждом запуске | Подписывает cookie сессий; secret_key в конфигурации имеет приоритет |
POTATO_ADMIN_API_KEY | сгенерированный файл | Ключ API администратора; admin_api_key в конфигурации имеет приоритет |
POTATO_GENERATED_TEMPLATES_DIR | выбирается при запуске | Куда Potato записывает шаблоны задач, которые строит при запуске |
POTATO_BUNDLE_URL | не задана | URL архива .tar.gz с проектом, который скачивается в /app при запуске |
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, например -p 8000:9000 для PORT=9000.
Число рабочих процессов и потоков
Контейнер отказывается стартовать с более чем одним рабочим процессом gunicorn. Potato держит пул объектов, очередь назначения и состояние каждого разметчика в памяти одного процесса. Второй рабочий процесс хранил бы собственную копию всех трёх и назначал бы объекты, которые первый уже назначил. Каждое сохранение целиком перезаписывает user_state.json, и тот процесс, который сохранял последним, без всякой ошибки стёр бы разметку другого. Чтобы обслужить больше одновременных запросов, поднимайте GUNICORN_THREADS, потому что потоки делят одну копию состояния.
Сгенерированные шаблоны
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 администратора принимают один общий ключ, который отправляется в заголовке X-API-Key или вводится на странице входа администратора. Potato читает ключ сначала из ключа верхнего уровня admin_api_key в конфигурации, затем из переменной окружения POTATO_ADMIN_API_KEY. В контейнере переменная окружения не пускает ключ ни в config.yaml, ни в репозиторий, где лежит проект. Файл potato.env из раздела про долгие исследования задаёт эту переменную, и ключом администратора становится то, что записано в этом файле:
grep POTATO_ADMIN_API_KEY potato.envЕсли ключ не задан ни в конфигурации, ни в окружении, Potato генерирует его, когда кто-нибудь впервые открывает /admin или вызывает конечную точку администратора. Сгенерированный ключ Potato записывает в admin_api_key.txt в каталоге задачи. В контейнере каталог задачи это /app, смонтированная папка проекта. Файл доступен для чтения только пользователю сервера, и после перезапуска Potato читает тот же файл. Журнал сервера называет файл и никогда не печатает ключ. Прочитайте ключ через контейнер:
docker exec potato cat /app/admin_api_key.txtНа хосте с Linux, где вы передали папку проекта uid 1000, ваша учётная запись не может прочитать admin_api_key.txt без sudo. Контейнер, запущенный с --user "$(id -u):$(id -g)", записывает файл от имени вашей учётной записи. Что открывает ключ, описано на странице Админ-панель.
Режим отладки в контейнере
Держите debug: false в любом контейнере, который обслуживает исследование. При debug: true в конфигурации Potato меняет поведение двух возможностей:
- Админ-страницы без ключа. Админ-панель и API администратора не требуют ключ для запроса с адреса loopback, в котором нет заголовков
X-Forwarded-For,X-Real-IPиForwarded. В сети bridge, которую Docker создаёт по умолчанию, запрос к опубликованному порту доходит до контейнера с адреса моста и по-прежнему требует ключ. С--network hostзапрос с машины хоста является запросом loopback и ключа не требует. Обратный прокси в том же сетевом пространстве имён, который не добавляет заголовков пересылки, делает локальным каждый запрос, который он передаёт дальше. - Общая отладочная учётная запись. Если задан ещё и
debug_phase, каждый посетитель входит в одну общую учётную запись,debug_user, и сразу попадает в эту фазу. Эта учётная запись не имеет прав администратора, но все посетители делят её ответы.
Всякий раз, когда debug включён, сервер пишет предупреждение при запуске. Команда, выполненная внутри контейнера через docker exec, это запрос loopback. В режиме отладки 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. Страница Production-настройка показывает файл Compose, который запускает этот образ за nginx, с папкой проекта в /app и ключами сессии и администратора, читаемыми из окружения.
Обновление до новой версии
Обновление Potato заменяет контейнер, а папку проекта со всей разметкой и базами данных оставляет на месте. Перед переключением скачайте новый релиз, проверьте конфигурацию валидатором нового релиза и сделайте снимок базы данных проекта. Замените 2.14.0 на релиз, на который переходите:
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Затем замените работающий контейнер контейнером с новым тегом, с тем же монтированием и тем же файлом окружения:
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.
Что почитать дальше
- Запуск Potato локально или в облаке - цели
potato deploy, которые запускают этот образ за вас - Production-настройка - gunicorn, nginx, TLS, сессии и резервные копии на вашем собственном сервере
- Обратный прокси - как обслуживать Potato под префиксом пути URL
- Админ-панель - что открывает ключ администратора
Подробности реализации см. в исходной документации.