Skip to content

Production-настройка

Как развернуть Potato в продакшене: gunicorn, nginx или Apache, терминация TLS, Docker, сохранение сессий и контроль доступа, который нужен общему серверу.

Как развернуть Potato для реальных задач разметки с несколькими пользователями, сохранностью данных и нормальной безопасностью.

Настройка сервера

Базовая production-конфигурация

yaml
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 ровно с одним рабочим процессом, а когда нужна большая параллельность, увеличивайте число потоков:

bash
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

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

Настройка обратного прокси

Конфигурация nginx

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

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 его публичный префикс. Если прокси под вашим управлением и может передавать заголовки пересылки:

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

Включайте POTATO_PROXY_FIX только тогда, когда приложение доступно исключительно через этот прокси. Он доверяет заголовкам пересылки, поэтому клиент, который может напрямую достучаться до внутреннего порта, способен подделать префикс и испортить каждый сгенерированный URL.

Если добавить заголовки нельзя, но путь монтирования известен, задайте его напрямую. Прокси всё равно должен отрезать префикс перед пересылкой:

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

Проверяйте, входя в браузере без сессии: форма входа ломается от отсутствующего префикса первой.

Просмотрщики живого агента и живого кодирования получают поток через Server-Sent Events, а буферизующий прокси задерживает его, пока поток не закончится. Отключите буферизацию на этих маршрутах:

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

Potato публикует готовый образ в GitHub Container Registry, ghcr.io/davidjurgens/potato. В нём есть Potato и его зависимости, и больше ничего. Папка вашего проекта с config.yaml и данными монтируется в /app, так что один образ обслуживает любую задачу:

bash
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.

Аргументы после имени образа заменяют команду сервера, что удобно для разовых проверок:

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

Внутри сети Compose nginx обращается к Potato по имени сервиса и порту контейнера, поэтому блок upstream из раздела про обратный прокси выше превращается в server potato:7860; вместо server 127.0.0.1:8000;.

Папка проекта монтируется, а не копируется в образ, поэтому разметка, которую Potato в неё записывает, остаётся на хосте, когда контейнер заменяют.

Управление доступом

Кто может зарегистрироваться

По умолчанию Potato разрешает самостоятельную регистрацию. На публичном сервере перечислите, кто может зарегистрироваться:

yaml
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-хосте задайте его сами и не храните в репозитории:

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

Когда настроен rbac, названные люди попадают в /admin, просто войдя в систему, и общий ключ передавать друг другу не нужно. Полный набор прав описан на странице Управление пользователями.

Сессии

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

secret_key подписывает cookie сессии и может браться из POTATO_SECRET_KEY. Без него Potato генерирует новый ключ при каждом запуске, так что каждый перезапуск разлогинивает всех посреди задачи. На сервере, который перезапускается при развёртывании, именно эта настройка чаще всего стоит потерянной разметки.

session_timeout_minutes завершает сессию, простоявшую без дела столько минут, и отсчёт начинается заново с каждым запросом. Potato 2.11 первым из релизов соблюдает эту настройку, со значением по умолчанию 480 минут. При 60, как в этом примере, разметчику, который отошёл на час, придётся войти снова, когда он вернётся.

Результаты разметки

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

Potato создаёт под output_annotation_dir по каталогу на каждого разметчика, и в каждом лежит user_state.json этого человека. export_annotation_format перечисляет форматы, которые записывает периодический экспорт, а пустое значение означает, что автоэкспорта нет вовсе. auto_export_interval задаёт промежуток между экспортами в секундах, по умолчанию 60.

База данных

По умолчанию разметка хранится на диске. Для развёртываний, которым нужна база данных, database.type принимает mysql или file:

yaml
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 в любом развёртывании, и этот маршрут не требует аутентификации, так что он подходит как проверка готовности для балансировщика нагрузки или оркестратора:

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

Подробность журнала задаётся в конфигурации или в командной строке, а не в отдельном блоке журналирования:

yaml
verbose: true
debug_log: server

debug_log принимает all, ui, server или none. Переменная окружения POTATO_LOGGERS сужает вывод до названных логгеров, когда вы разбираетесь с одной подсистемой.

Масштабирование

Potato работает как один процесс, который держит весь набор объектов в памяти, поэтому масштабируйте машину, а не число серверов. Несколько реплик за балансировщиком нагрузки не делят работу между собой. Каждая реплика хранит свою копию очереди назначений и состояния разметчиков, и это воспроизводит ровно ту потерю данных, что описана в разделе про gunicorn выше.

С размером датасета растёт память. Примерно с миллиона объектов их содержимое можно перенести в кэш SQLite:

yaml
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 каждые пять минут, это интервал по умолчанию, так что потеря диска стоит разметчикам не больше пяти минут работы:

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 направляет приёмник в любой сервис, который понимает 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:

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 {} +

Запускайте его из cron каждую ночь в 2 часа:

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

Ночная копия может потерять до суток работы, поэтому во время исследования с большим числом разметчиков запускайте её чаще. До начала исследования восстановите одну копию на тестовом сервере и убедитесь, что последняя разметка на месте.

Чек-лист безопасности

  • Терминируйте TLS на прокси с действующим сертификатом
  • Задайте secret_key или POTATO_SECRET_KEY, чтобы сессии переживали перезапуск
  • Задайте admin_api_key, а не отдавайте сгенерированный файл
  • Ограничьте регистрацию через user_config.users
  • Держите debug выключенным, потому что режим отладки пускает запросы обратного прокси в админ-страницы без ключа
  • Запускайте gunicorn с одним рабочим процессом
  • Храните учётные данные в переменных окружения, а не в конфигурации
  • Включайте POTATO_PROXY_FIX только за доверенным прокси
  • Делайте резервные копии выходного каталога, файла учётных записей и баз данных и проверьте восстановление
  • Проверьте, у кого есть роль администратора

Переменные окружения

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

Сгенерировать ключ подписи можно так:

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

Загрузка в Docker Compose:

yaml
services:
  potato:
    env_file:
      - .env

Устранение неполадок

Просмотр журналов

bash
# 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, если объектов больше примерно миллиона.

Что дальше

Подробности реализации см. в руководстве по обратному прокси и руководстве по масштабированию.