Skip to content

Ручная установка Callbox с Docker Compose

Инструкция предназначена для NextBox oak-1.11.0, уже установленного по основной инструкции Docker Compose. Для новой production-установки предпочтителен установщик NextBox: он автоматически генерирует секреты, TURN/TLS и согласованные конфигурации LiveKit.

Перед началом сделайте резервную копию docker-compose.yaml, default.conf и PostgreSQL.

1. Подготовить секреты и базу

Основная инструкция уже добавляет в .env необходимые значения:

  • JWT_SIGN_KEY — общий ключ пользовательских JWT NextBox;
  • SERVICE_TOKEN — общий токен межсервисных запросов;
  • POSTGRES_PASSWORD и RABBITMQ_PASSWORD — пароли инфраструктурных сервисов;
  • LIVEKIT_API_KEY — идентификатор ключа, по умолчанию livekit-prod;
  • LIVEKIT_API_SECRET — случайный секрет длиной не менее 32 байт.

Если .env был создан по старой версии инструкции и переменных LiveKit в нем нет, добавьте их, не изменяя существующие секреты:

bash
if ! grep -q '^LIVEKIT_API_KEY=' .env; then
  printf 'LIVEKIT_API_KEY=livekit-prod\n' >>.env
fi

if ! grep -q '^LIVEKIT_API_SECRET=' .env; then
  printf 'LIVEKIT_API_SECRET=%s\n' "$(openssl rand -hex 32)" >>.env
fi

chmod 600 .env

Создайте базу один раз. Команда ниже рассчитана на параметры из основной инструкции NextBox:

bash
docker compose exec postgresql \
  psql -U nextbox -d postgres \
  -c "CREATE DATABASE callbox OWNER nextbox;"

Если база уже существует, не выполняйте команду повторно. Миграции таблиц Callbox применит автоматически.

2. Создать конфигурации LiveKit и Egress

Docker Compose подставляет .env в docker-compose.yaml, но не подставляет переменные в произвольные YAML-файлы. Сохраните следующий скрипт как generate-callbox-configs.sh: он читает ключи из .env, проверяет их формат и атомарно создаёт livekit.yaml и egress.yaml.

bash
#!/usr/bin/env bash
set -Eeuo pipefail

for file in .env; do
  [[ -f "$file" ]] || { echo "Ошибка: не найден $file." >&2; exit 1; }
done

for file in livekit.yaml egress.yaml; do
  [[ ! -e "$file" ]] || {
    echo "Ошибка: $file уже существует; конфигурация не изменена." >&2
    exit 1
  }
done

read_env() {
  local name=$1 value
  value=$(sed -n "s/^${name}=//p" .env)
  [[ -n "$value" ]] || { echo "Ошибка: в .env отсутствует $name." >&2; exit 1; }
  [[ "$value" =~ ^[A-Za-z0-9._-]+$ ]] || {
    echo "Ошибка: $name содержит недопустимые символы." >&2
    exit 1
  }
  printf '%s' "$value"
}

livekit_api_key=$(read_env LIVEKIT_API_KEY)
livekit_api_secret=$(read_env LIVEKIT_API_SECRET)
[[ ${#livekit_api_secret} -ge 32 ]] || {
  echo "Ошибка: LIVEKIT_API_SECRET должен содержать не менее 32 символов." >&2
  exit 1
}

umask 077
livekit_tmp=$(mktemp livekit.yaml.tmp.XXXXXX)
egress_tmp=$(mktemp egress.yaml.tmp.XXXXXX)
trap 'rm -f "$livekit_tmp" "$egress_tmp"' EXIT

cat >"$livekit_tmp" <<EOF
port: 7880
bind_addresses:
  - ""

rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: false
  enable_loopback_candidate: false

redis:
  address: redis:6379
  db: 1

keys:
  ${livekit_api_key}: ${livekit_api_secret}

webhook:
  api_key: ${livekit_api_key}
  urls:
    - http://callbox:8081/callbox/api/egress/webhook
EOF

cat >"$egress_tmp" <<EOF
logging:
  level: info

api_key: ${livekit_api_key}
api_secret: ${livekit_api_secret}
ws_url: ws://livekit:7880
insecure: true

redis:
  address: redis:6379
  db: 1

cpu_cost:
  room_composite_cpu_cost: 4.0
EOF

mv -- "$livekit_tmp" livekit.yaml
mv -- "$egress_tmp" egress.yaml
trap - EXIT
echo "Созданы livekit.yaml и egress.yaml."

Запустите скрипт из директории установки:

bash
chmod +x generate-callbox-configs.sh
./generate-callbox-configs.sh

Получившийся livekit.yaml имеет следующую структуру:

yaml
port: 7880
bind_addresses:
  - ""

rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: false
  enable_loopback_candidate: false

redis:
  address: redis:6379
  db: 1

keys:
  <LIVEKIT_API_KEY>: <LIVEKIT_API_SECRET>

webhook:
  api_key: <LIVEKIT_API_KEY>
  urls:
    - http://callbox:8081/callbox/api/egress/webhook

Для сервера с публичным IP установите rtc.use_external_ip: true. Если на хосте несколько сетевых интерфейсов, дополнительно задайте rtc.node_ip публичным адресом, доступным клиентам.

Встроенный TURN и TURN over TLS в этом сокращённом примере не настроены. Для сетей со строгими firewall используйте установщик NextBox либо добавьте TURN по рекомендациям LiveKit.

Получившийся egress.yaml использует те же ключи, что и livekit.yaml:

yaml
logging:
  level: info

api_key: <LIVEKIT_API_KEY>
api_secret: <LIVEKIT_API_SECRET>
ws_url: ws://livekit:7880
insecure: true

redis:
  address: redis:6379
  db: 1

cpu_cost:
  room_composite_cpu_cost: 4.0

3. Добавить сервисы в Compose

Добавьте блоки внутрь существующей секции services:. Значения ${...} Docker Compose возьмёт из .env основной установки.

yaml
  livekit:
    image: livekit/livekit-server:v1.10.1
    restart: always
    command: --config /etc/livekit.yaml
    ports:
      - "7880:7880"
      - "7881:7881/tcp"
      - "50100-50200:50100-50200/udp"
    volumes:
      - ./livekit.yaml:/etc/livekit.yaml:ro
      - livekit_data:/var/livekit
    networks:
      - nextbox
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:7880"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 10s

  egress:
    image: livekit/egress:latest
    restart: always
    cpus: "4.0"
    shm_size: "1gb"
    environment:
      EGRESS_CONFIG_FILE: /etc/egress.yaml
    volumes:
      - ./egress.yaml:/etc/egress.yaml:ro
      - egress_data:/data
    cap_add:
      - SYS_ADMIN
    networks:
      - nextbox
    depends_on:
      livekit:
        condition: service_healthy
      redis:
        condition: service_healthy

  egress-files:
    image: nextbox-docker.ptnl.moscow/nextbox/callbox-egress-files:oak-1.11.0
    restart: always
    environment:
      BEARER_TOKEN: ${LIVEKIT_API_KEY}
      DATA_DIR: /data
      PORT: "9100"
      MAX_AGE: "60m"
      CLEANUP_INTERVAL: "10m"
    volumes:
      - egress_data:/data
    networks:
      - nextbox
    depends_on:
      egress:
        condition: service_started

  callbox:
    image: nextbox-docker.ptnl.moscow/nextbox/callbox:oak-1.11.0
    restart: always
    ports:
      - "8081:8081"
    environment:
      PORT: "8081"
      BASE_PATH: "/callbox"
      APP_VERSION: "1.11.0"
      LOG_LEVEL: "info"
      JWT_SECRET: ${JWT_SIGN_KEY}
      JWT_COOKIE_NAME: "access_token"

      DATABASE_DSN: "postgres://nextbox:${POSTGRES_PASSWORD}@postgresql:5432/callbox?sslmode=disable"
      DATABASE_MAX_OPEN_CONNS: "20"
      DATABASE_MAX_IDLE_CONNS: "10"
      DATABASE_CONN_MAX_IDLE_TIME: "5m"
      DATABASE_CONN_MAX_LIFETIME: "30m"

      REDIS_URL: "redis://redis:6379/0"
      REDIS_KEY_PREFIX: "callbox:v1"

      LIVEKIT_SERVER_URL: "http://livekit:7880"
      LIVEKIT_CLIENT_URL: "ws://localhost:7880"
      LIVEKIT_API_KEY: ${LIVEKIT_API_KEY}
      LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}

      NEXTBOX_API_URL: "http://nginx:80"
      FILE_STORAGE_PATH: "/var/callbox/files"
      CANVAS_STORAGE_PATH: "/var/callbox/canvases"
      FILE_MAX_SIZE: "1073741824"

      RECORDING_ENABLED: "false"
      EGRESS_FILES_URL: "http://egress-files:9100"

      DISCOVERY_URL: "http://discovery:6011"
      SERVICE_TOKEN: ${SERVICE_TOKEN}
      SERVICE_ROUTE_NAME: "callbox"
      SERVICE_BACK_URL: "http://callbox:8081"
      SERVICE_INSTANCE_NUMBER: "1"

      NEXTBOX_NOTIFICATIONS_ENABLED: "true"
      RABBITMQ_URL: "amqp://nextbox:${RABBITMQ_PASSWORD}@rabbitmq:5672/nextbox"
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          wget -q -O /dev/null
          --header="Authorization: Service $$SERVICE_TOKEN"
          http://127.0.0.1:8081/callbox/api/health-check
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    volumes:
      - callbox_files:/var/callbox/files
      - callbox_canvases:/var/callbox/canvases
    networks:
      - nextbox
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      discovery:
        condition: service_healthy
      livekit:
        condition: service_healthy

Добавьте volumes в существующую корневую секцию volumes::

yaml
volumes:
  # существующие volumes NextBox
  callbox_files:
  callbox_canvases:
  livekit_data:
  egress_data:

Проверки livekit и callbox выполняются через wget. Образы egress и egress-files не предоставляют отдельного HTTP health endpoint, поэтому их состояние проверяйте командами docker compose ps и docker compose logs.

Одинаковые значения

LIVEKIT_API_KEY должен совпадать в livekit.yaml, egress.yaml, Callbox и BEARER_TOKEN сервиса egress-files. LIVEKIT_API_SECRET должен совпадать в livekit.yaml, egress.yaml и Callbox. Скрипт и Compose получают оба значения из одного .env.

Общий Redis

LiveKit и Egress должны использовать одинаковые address: redis:6379 и db: 1. Не направляйте только один из них в другой Redis или другую DB: Egress использует Redis для связи с LiveKit и очередей заданий.

NEXTBOX_API_URL=http://nginx:80 намеренно указывает на внутренний reverse proxy, а не только на Gateway: Egress должен открыть встроенный шаблон по пути /callbox/static/egress/.

Добавить Whisper (опционально)

Для транскрибации записей добавьте сервис Whisper в секцию services::

yaml
  whisper:
    image: nextbox-docker.ptnl.moscow/nextbox/callbox-whisper-cpu:oak-1.11.0
    restart: always
    environment:
      HF_TOKEN: ${HF_TOKEN:-}
      TRANSCRIPTION_ENGINE: "whisperx"
      DEVICE: "cpu"
      COMPUTE_TYPE: "int8"
      MODEL_SIZE: "large-v3"
      WHISPER_LANGUAGE: ""
      HF_HOME: "/data/models"
      TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD: "1"
    volumes:
      - whisper_models:/data/models
    networks:
      - nextbox
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)"]
      interval: 15s
      timeout: 10s
      start_period: 5m
      retries: 5

Добавьте в environment: сервиса callbox:

yaml
      WHISPER_URL: "http://whisper:8000"
      WHISPER_SPEED_FACTOR: "1.0"
      WHISPER_MAX_TIMEOUT: "2h"
      TRANSCRIPTION_WORKER_CONCURRENCY: "1"
      TRANSCRIPTION_MAX_ATTEMPTS: "3"
      TRANSCRIPTION_HISTORY_RETENTION_DAYS: "180"

и зависимость Callbox от готовности Whisper:

yaml
    depends_on:
      whisper:
        condition: service_healthy

В корневую секцию volumes: добавьте постоянный cache моделей:

yaml
volumes:
  whisper_models:

Первый запуск может занять несколько минут, пока модель загружается в whisper_models. HF_TOKEN нужен только для загрузки моделей pyannote и разделения результата по спикерам; без него основная транскрибация продолжит работать.

Для NVIDIA GPU используйте образ nextbox-docker.ptnl.moscow/nextbox/callbox-whisper-cuda:oak-1.11.0, измените DEVICE на cuda, COMPUTE_TYPE на float16, добавьте gpus: all, а в healthcheck замените python на python3.11. На хосте должны быть установлены драйвер NVIDIA и NVIDIA Container Toolkit.

Вместо локального контейнера можно указать внешний совместимый сервис:

yaml
WHISPER_URL: "http://transcription-gpu.internal:8000"

Callbox ожидает от него GET /health и POST /v1/transcribe. Не публикуйте этот сервис в интернет без отдельной аутентификации и TLS.

4. Настроить reverse proxy

Добавьте в существующий server { ... } файла default.conf маршрут Callbox:

nginx
location /callbox {
    proxy_pass http://callbox:8081;
    proxy_http_version 1.1;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $proxy_connection;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $the_host;
    proxy_set_header X-Forwarded-Proto $the_scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_request_buffering off;
    proxy_read_timeout 900s;
    proxy_send_timeout 900s;
}

LiveKit в примере доступен клиентам напрямую на ws://<host>:7880. Для HTTPS-развёртывания настройте отдельный TLS-host, например wss://calls.example.com, который проксирует корень на livekit:7880, и укажите его в LIVEKIT_CLIENT_URL. Не используйте ws:// со страницей NextBox, открытой по HTTPS: браузер заблокирует mixed content.

5. Проверить и запустить

bash
docker compose config --quiet
docker compose pull
docker compose up -d --wait
docker compose restart nginx
docker compose ps

Если Whisper включён, проверьте его из контейнера Callbox:

bash
docker compose exec callbox wget -qO- http://whisper:8000/health

Проверьте API и регистрацию Discovery:

bash
set -a
. ./.env
set +a
curl -fsS \
  -H "Authorization: Service ${SERVICE_TOKEN}" \
  http://127.0.0.1:8081/callbox/api/health-check

Интерфейс будет доступен по адресу http://localhost:8095/callbox, если основной reverse proxy NextBox опубликован на порту 8095.

6. Включить запись

После базовой проверки измените:

yaml
RECORDING_ENABLED: "true"

и пересоздайте Callbox:

bash
docker compose up -d callbox
docker compose logs -f --tail=100 callbox egress egress-files

Создайте короткую тестовую запись. Успешный сценарий выглядит так: Egress завершает файл в egress_data, webhook приходит в Callbox, Callbox скачивает файл с Bearer-токеном и сохраняет его в callbox_files.

Обновление

Перед обновлением сохраните PostgreSQL и конфигурационные файлы, затем смените теги обоих образов NextBox Callbox на одну и ту же версию:

bash
docker compose pull callbox egress-files
docker compose up -d --wait callbox egress-files
docker compose logs --tail=100 callbox

Не удаляйте volumes callbox_files, callbox_canvases и базу callbox при обычном обновлении.

Диагностика

  • Callbox не стартует: проверьте пять обязательных переменных и доступность базы callbox.
  • Discovery видит сервис как unhealthy: проверьте SERVICE_BACK_URL=http://callbox:8081, общий SERVICE_TOKEN и ответ корневого endpoint /api/health-check.
  • Клиент не подключается к звонку: проверьте LIVEKIT_CLIENT_URL, TCP 7880/7881, UDP 50100-50200 и отсутствие mixed content.
  • Запись не появляется: проверьте webhook в livekit.yaml, совпадение ключей, общий egress_data и логи трёх сервисов.
  • LiveKit не видит Egress: проверьте, что в livekit.yaml и egress.yaml одинаково указаны redis:6379 и DB 1.
  • Egress возвращает 401: BEARER_TOKEN должен совпадать с LIVEKIT_API_KEY.
  • Видео Egress пустое: убедитесь, что http://nginx/callbox/static/egress/ открывается из контейнера egress и что NEXTBOX_API_URL маршрутизирует /callbox.
  • Транскрибация недоступна: проверьте WHISPER_URL, healthcheck и логи сервисов whisper и callbox. При первом запуске дождитесь загрузки модели.