Production-настройка
Как развернуть Potato в продакшене: gunicorn, nginx или Apache, терминация TLS, Docker, сохранение сессий и контроль доступа, который нужен общему серверу.
Как развернуть Potato для реальных задач разметки с несколькими пользователями, сохранностью данных и нормальной безопасностью.
Настройка сервера
Базовая production-конфигурация
server:
host: 0.0.0.0
port: 8000
debug: falseБлок server принимает host, port и debug, и больше ничего. Число рабочих процессов и тайм-ауты задаются в том WSGI-сервере, который запускает Potato, а не в файле конфигурации. В продакшене держите debug выключенным: режим отладки открывает админ-страницы без ключа для запросов с той же машины, где работает сервер, а на развёрнутом хосте к ним относится и его обратный прокси.
HTTPS
В конфигурации Potato нет настроек TLS, поэтому сертификаты там не задаются. Терминируйте TLS на обратном прокси перед Potato, как это делается в разделах про nginx и Apache ниже, или передайте сертификат напрямую gunicorn, как показано в разделе «С SSL».
Запуск под gunicorn
Запускайте Potato под gunicorn ровно с одним рабочим процессом, а когда нужна большая параллельность, увеличивайте число потоков:
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:8000 \
--timeout 120Один рабочий процесс нужен для корректности, а не для тонкой настройки. Potato держит пул объектов, очередь назначений и состояние каждого разметчика в памяти внутри процесса. Второй рабочий процесс получает собственную копию всех трёх, поэтому выдаёт объекты, которые первый уже назначил, а поскольку user_state.json при каждом сохранении перезаписывается целиком, тот процесс, что сохраняет последним, затирает разметку другого. Об этой потере ничто не сообщает, разметка просто пропадает. Потоки делят одну копию этого состояния, поэтому --threads безопасно повышает параллельность, а --workers нет.
С SSL
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Настройка обратного прокси
Конфигурация 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
<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>Монтирование под путём URL
Чтобы открыть Potato по адресу https://host/app1/, а не в корне домена, нужен ещё один шаг. Страница разметки запрашивает /static/... и /updateinstance относительно публичного корня, поэтому под префиксом пути ресурсы отдают 404, а разметка не сохраняется, хотя каркас страницы по-прежнему отрисовывается.
Сообщите Potato его публичный префикс. Если прокси под вашим управлением и может передавать заголовки пересылки:
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000location /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;
}Включайте POTATO_PROXY_FIX только тогда, когда приложение доступно исключительно через этот прокси. Он доверяет заголовкам пересылки, поэтому клиент, который может напрямую достучаться до внутреннего порта, способен подделать префикс и испортить каждый сгенерированный URL.
Если добавить заголовки нельзя, но путь монтирования известен, задайте его напрямую. Прокси всё равно должен отрезать префикс перед пересылкой:
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000Проверяйте, входя в браузере без сессии: форма входа ломается от отсутствующего префикса первой.
Просмотрщики живого агента и живого кодирования получают поток через Server-Sent Events, а буферизующий прокси задерживает его, пока поток не закончится. Отключите буферизацию на этих маршрутах:
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
Potato публикует готовый образ в GitHub Container Registry, ghcr.io/davidjurgens/potato. В нём есть Potato и его зависимости, и больше ничего. Папка вашего проекта с config.yaml и данными монтируется в /app, так что один образ обслуживает любую задачу:
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestТег latest и теги релизов, например 2.12.1, содержат только основные зависимости. Теги -all (latest-all, 2.12.1-all) добавляют SDK для ИИ, читатели форматов, а также дополнения для экспорта и аутентификации, но не включают модели компьютерного зрения.
Контейнер обслуживает задачу одним рабочим процессом gunicorn с восемью потоками по причине, описанной в разделе про gunicorn выше. Если задать GUNICORN_WORKERS больше 1, контейнер остановится при запуске, если только не задан ещё и POTATO_ALLOW_MULTIWORKER=1, а с ним в настоящем исследовании теряется разметка. Для большей параллельности увеличивайте GUNICORN_THREADS. POTATO_CONFIG указывает файл конфигурации с именем, отличным от config.yaml, а PORT меняет порт внутри контейнера с 7860 на другой.
Контейнер работает от uid 1000 и записывает результаты разметки, potato.log и свои базы данных SQLite в /app. На Linux примонтированная папка проекта, принадлежащая другому пользователю, останавливает сервер при запуске с ошибкой, которая называет причину. Передайте папку uid 1000 командой sudo chown -R 1000:1000 myproject. Docker Desktop на Mac не учитывает владельца примонтированных папок, поэтому ошибка возникает только на Linux.
Аргументы после имени образа заменяют команду сервера, что удобно для разовых проверок:
docker run -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strictDocker Compose
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Внутри сети Compose nginx обращается к Potato по имени сервиса и порту контейнера, поэтому блок upstream из раздела про обратный прокси выше превращается в server potato:7860; вместо server 127.0.0.1:8000;.
Папка проекта монтируется, а не копируется в образ, поэтому разметка, которую Potato в неё записывает, остаётся на хосте, когда контейнер заменяют.
Управление доступом
Кто может зарегистрироваться
По умолчанию Potato разрешает самостоятельную регистрацию. На публичном сервере перечислите, кто может зарегистрироваться:
user_config:
allow_all_users: false
users:
- annotator1@example.com
- annotator2@example.comОба ключа должны лежать под user_config. На верхнем уровне они игнорируются, и получается список доступа, который на деле пускает всех.
Доступ администратора
admin_api_key задаёт общий ключ, который передаётся в заголовке X-API-Key. Если вы его не задали, Potato генерирует ключ и записывает его в {task_dir}/admin_api_key.txt, поэтому на production-хосте задайте его сами и не храните в репозитории:
admin_api_key: "your-shared-key"
rbac:
enabled: true
user_role_assignments:
lead@example.com: adminКогда настроен rbac, названные люди попадают в /admin, просто войдя в систему, и общий ключ передавать друг другу не нужно. Полный набор прав описан на странице Управление пользователями.
Сессии
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200secret_key подписывает cookie сессии и может браться из POTATO_SECRET_KEY. Без него Potato генерирует новый ключ при каждом запуске, так что каждый перезапуск разлогинивает всех посреди задачи. На сервере, который перезапускается при развёртывании, именно эта настройка чаще всего стоит потерянной разметки.
session_timeout_minutes завершает сессию, простоявшую без дела столько минут, и отсчёт начинается заново с каждым запросом. Potato 2.11 первым из релизов соблюдает эту настройку, со значением по умолчанию 480 минут. При 60, как в этом примере, разметчику, который отошёл на час, придётся войти снова, когда он вернётся.
Результаты разметки
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300Potato создаёт под output_annotation_dir по каталогу на каждого разметчика, и в каждом лежит user_state.json этого человека. export_annotation_format перечисляет форматы, которые записывает периодический экспорт, а пустое значение означает, что автоэкспорта нет вовсе. auto_export_interval задаёт промежуток между экспортами в секундах, по умолчанию 60.
База данных
По умолчанию разметка хранится на диске. Для развёртываний, которым нужна база данных, database.type принимает mysql или file:
database:
type: mysql
host: localhost
port: 3306
database: potato_db
username: potato
password: ${DB_PASSWORD}Ключи называются database и username, а не name и user. Строку подключения можно вместо этого передать через POTATO_DB_CONNECTION, и тогда учётные данные не попадают в файл конфигурации.
Установите драйвер командой pip install 'potato-annotation[mysql]'. До 2.10.2 бэкенд MySQL так и не включался, и конфигурация, где он был указан, молча хранила разметку на диске. Начиная с 2.10.2 отсутствующий драйвер или недоступный сервер останавливают запуск, а экспорт, резервные копии и potato deploy pull читают базу данных.
Проверки состояния и журналы
Potato отвечает на GET /health в любом развёртывании, и этот маршрут не требует аутентификации, так что он подходит как проверка готовности для балансировщика нагрузки или оркестратора:
curl -sf https://annotation.example.com/healthПодробность журнала задаётся в конфигурации или в командной строке, а не в отдельном блоке журналирования:
verbose: true
debug_log: serverdebug_log принимает all, ui, server или none. Переменная окружения POTATO_LOGGERS сужает вывод до названных логгеров, когда вы разбираетесь с одной подсистемой.
Масштабирование
Potato работает как один процесс, который держит весь набор объектов в памяти, поэтому масштабируйте машину, а не число серверов. Несколько реплик за балансировщиком нагрузки не делят работу между собой. Каждая реплика хранит свою копию очереди назначений и состояния разметчиков, и это воспроизводит ровно ту потерю данных, что описана в разделе про gunicorn выше.
С размером датасета растёт память. Примерно с миллиона объектов их содержимое можно перенести в кэш SQLite:
item_store:
backend: paged # default: memory
cache_size: 2048Прежде чем включать это, сделайте замеры. Постраничное хранение экономит примерно 28% на объектах компьютерного зрения и 51% на тексте, а не на порядок, и замедляет загрузку корпуса примерно в 2,5 раза. Файл служит кэшем и пересобирается из файлов данных при каждом запуске, поэтому удалять его безопасно.
Для больших проектов работают другие рычаги: ограничить то, что назначается одному разметчику, через per_annotator_quota, разделить корпус между когортами через batch_assignment и добавлять файлы данных постепенно через data_directory, а не загружать всё при запуске.
Резервное копирование
Резервная копия должна охватывать не только выходной каталог разметки. В каталоге задачи также лежат до трёх баз данных SQLite, и ни одну из них нельзя восстановить из файлов данных: project.sqlite (заметки, кодбук, процесс рецензирования), datasets.sqlite и engine.sqlite, если используется движок разметки. Учётные записи, созданные через форму регистрации, Potato хранит в user_config.json. Пока сервер пишет в базы данных, они работают в режиме WAL, поэтому tar или cp файла базы данных может дать копию, повреждённую или без последних изменений, и ни одна из этих команд вас не предупредит.
Встроенное резервное копирование
Начиная с версии 2.10 блок backup в Potato копирует результаты разметки и снимки баз данных, сделанные через .backup, за пределы сервера и восстанавливает их, когда сервер стартует с пустым диском. Вместе с разметкой восстанавливается и список учётных записей, а разметку, которая уже лежит на диске, восстановление никогда не перезаписывает. Блок ниже отправляет копию в бакет S3 каждые пять минут, это интервал по умолчанию, так что потеря диска стоит разметчикам не больше пяти минут работы:
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 S3endpoint_url направляет приёмник в любой сервис, который понимает API S3, например в университетское объектное хранилище, MinIO, Cloudflare R2 или Backblaze B2. Учётные данные берутся из POTATO_S3_ACCESS_KEY_ID и POTATO_S3_SECRET_ACCESS_KEY в окружении сервера или из обычной цепочки boto3. Ключ, ограниченный одним этим бакетом, сужает то, до чего может добраться сервер. Приёмник type: huggingface с repo_id отправляет копию в приватный датасет Hugging Face, используя HF_TOKEN. Обоим приёмникам нужно дополнение hosting, pip install 'potato-annotation[hosting]'.
Копия на вашем собственном хранилище
Если соглашение об использовании данных требует, чтобы данные оставались на сервере, ночное задание может копировать их на локальное или примонтированное хранилище. Скрипт ниже архивирует выходной каталог, снимает копию каждой базы данных командой SQLite .backup, а не копированием файла, и хранит копии за семь дней. Ему нужна консольная утилита sqlite3:
#!/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 {} +Запускайте его из cron каждую ночь в 2 часа:
0 2 * * * /var/potato/backup.shНочная копия может потерять до суток работы, поэтому во время исследования с большим числом разметчиков запускайте её чаще. До начала исследования восстановите одну копию на тестовом сервере и убедитесь, что последняя разметка на месте.
Чек-лист безопасности
- Терминируйте TLS на прокси с действующим сертификатом
- Задайте
secret_keyилиPOTATO_SECRET_KEY, чтобы сессии переживали перезапуск - Задайте
admin_api_key, а не отдавайте сгенерированный файл - Ограничьте регистрацию через
user_config.users - Держите
debugвыключенным, потому что режим отладки пускает запросы обратного прокси в админ-страницы без ключа - Запускайте gunicorn с одним рабочим процессом
- Храните учётные данные в переменных окружения, а не в конфигурации
- Включайте
POTATO_PROXY_FIXтолько за доверенным прокси - Делайте резервные копии выходного каталога, файла учётных записей и баз данных и проверьте восстановление
- Проверьте, у кого есть роль администратора
Переменные окружения
# .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Сгенерировать ключ подписи можно так:
python3 -c 'import secrets; print(secrets.token_hex(32))'Загрузка в Docker Compose:
services:
potato:
env_file:
- .envУстранение неполадок
Просмотр журналов
# Gunicorn logs
journalctl -u potato -f
# Docker logs
docker-compose logs -f potatoЧастые проблемы
502 Bad Gateway: проверьте, что Potato запущен и слушает тот порт, на который пересылает прокси.
После развёртывания всех разлогинило: secret_key не задан, поэтому при запуске сгенерировался новый ключ подписи.
Пропала разметка: проверьте число рабочих процессов. Больше одного рабочего процесса gunicorn молча отбрасывают разметку.
Ресурсы отдают 404 под префиксом пути: задайте POTATO_PROXY_FIX или POTATO_URL_PREFIX и убедитесь, что прокси отрезает префикс перед пересылкой.
Медленно работает на большом корпусе: добавляйте память раньше, чем серверы, и подумайте о item_store.backend: paged, если объектов больше примерно миллиона.
Что дальше
- Управление пользователями: списки доступа, роли и квоты
- Краудсорсинг: Prolific, SONA и другие панели
- Форматы данных: хранение и экспорт
Подробности реализации см. в руководстве по обратному прокси и руководстве по масштабированию.