Документ описывает продакшен-разворот одним файлом docker-compose.prod.yml: MCP-сервер и бот внутри Docker-сети, наружу выставлены только порты 80 и 443 через Caddy. Режимы Qwen, монтирование знаний и QWEN_MODE=container (Docker socket) описаны ниже.
- Клонируйте репозиторий API и перейдите в его корень (каталог, где лежат
Dockerfileиdocker-compose.prod.yml). - Создайте файл
.envв этом же каталоге — он используется так:- Подстановка в Compose (
${VAR}вdocker-compose.prod.yml, в т.ч.PUBLIC_HOSTдля сервиса caddy) читается из.envв корне проекта автоматически. - Переменные внутри контейнеров приложения задаются директивой
env_file: .envу сервисовmcp-serverиbot(caddy получает толькоPUBLIC_HOST, без остальных секретов).
- Подстановка в Compose (
- Скопируйте шаблон и заполните значения:
cp .env.example .env
chmod 600 .env- Создайте каталог состояния и при необходимости наполните
knowledge/:
mkdir -p data/state knowledge- Запуск и проверка:
docker compose -f docker-compose.prod.yml up -d --build
curl -fsS http://127.0.0.1/healthДля схемы HTTPS с доменом см. раздел 4; до тех пор по умолчанию в .env задаётся PUBLIC_HOST=:80 (только HTTP на порту 80).
| Сервис | Назначение |
|---|---|
qwen-search |
Одноразовый job: собирает образ spawndock/qwen-search:prod для вызовов docker run из MCP. |
mcp-server |
Control plane, MCP (/sse), туннель preview, поиск по базе знаний. Порт 3000 доступен только внутри сети Compose. |
bot |
Telegram polling-бот; CONTROL_PLANE_URL=http://mcp-server:3000 (внутренняя сеть). |
caddy |
Публикует 80/tcp и 443/tcp, проксирует на mcp-server:3000. Адрес сайта задаётся PUBLIC_HOST в .env. |
Ниже — переменные из .env.example с пояснениями. Все чувствительные значения храните только в .env, не коммитьте его.
| Переменная | Обязательность | Описание |
|---|---|---|
PUBLIC_HOST |
Рекомендуется | Адрес «сайта» для Caddy: :80 — HTTP на всех интерфейсах (порт 80); api.example.com — то же имя в DNS на эту машину → Caddy запросит Let’s Encrypt и поднимет HTTPS. |
QWEN_KNOWLEDGE_HOST_PATH |
Если QWEN_MODE=container |
Абсолютный путь на хосте к каталогу знаний (тот же физический каталог, что монтируется как ./knowledge:/app/knowledge:ro). Нужен для docker run -v со стороны демона Docker. Пример: /srv/spawndock-api/knowledge. |
| Переменная | Описание |
|---|---|
PORT |
Порт HTTP внутри контейнера (обычно 3000); снаружи доступ через Caddy. |
RATE_LIMIT_RPS |
Лимит запросов в секунду. |
PUBLIC_ORIGIN |
Публичный URL без завершающего / (как клиенты открывают API), например https://api.example.com или http://203.0.113.10. Должен совпадать со схемой и хостом, которые реально используются за Caddy. |
STATE_FILE |
Путь к JSON состояния внутри контейнера; в Compose обычно /app/.spawndock/state.json (том ./data/state). |
SPAWNDOCK_BOT_SECRET |
Секрет для внутренних вызовов control plane (сгенерируйте случайную строку). |
| Переменная | Описание |
|---|---|
QWEN_MODE |
http — внешний API; container — изолированный docker run образа поиска; cli — бинарь внутри образа API (в prod обычно не используется). |
QWEN_API_URL |
Базовый URL API при QWEN_MODE=http. |
HF_TOKEN |
Токен Hugging Face (если провайдер им требуется). |
QWEN_TIMEOUT_MS |
Таймаут запроса к модели. |
QWEN_CODE_COMMAND, QWEN_CODE_AUTH_TYPE |
Настройки CLI в режиме container/cli. |
QWEN_CONTAINER_IMAGE |
Образ для QWEN_MODE=container (в Compose по умолчанию переопределяется на spawndock/qwen-search:prod). |
QWEN_CONTAINER_RUNTIME |
docker или podman. |
QWEN_CONTAINER_CORPUS_PATH |
Путь монтирования корпуса внутри одноразового контейнера поиска. |
QWEN_CONTAINER_MAX_STDOUT_BYTES, QWEN_CONTAINER_MAX_STDERR_BYTES |
Лимиты вывода CLI. |
| Переменная | Описание |
|---|---|
TELEGRAM_BOT_TOKEN |
Токен от BotFather. |
TELEGRAM_BOT_USERNAME |
Username бота (без @). |
TELEGRAM_MINI_APP_SHORT_NAME |
Short name Mini App. |
CONTROL_PLANE_URL |
В Compose для сервиса bot задаётся в docker-compose.prod.yml как http://mcp-server:3000; в .env.example значение для локального запуска без Compose. |
BOT_POLL_TIMEOUT |
Таймаут long polling. |
| Переменная | Описание |
|---|---|
DOCKER_GID |
GID группы docker на хосте (getent group docker | cut -d: -f3), если убираете user: "0:0" у mcp-server и добавляете group_add в compose (см. раздел 7). |
Полный перечень ключей с плейсхолдерами см. в .env.example.
- Только HTTP (по IP или без TLS): в
.envукажитеPUBLIC_HOST=:80иPUBLIC_ORIGIN=http://<ваш-хост-или-IP>(без:3000). Проверка:curl -fsS http://<IP>/health. - HTTPS (Let’s Encrypt): запись DNS A/AAAA для вашего имени должна указывать на эту машину; откройте 80 и 443 на firewall. В
.env:
PUBLIC_HOST=api.example.com
PUBLIC_ORIGIN=https://api.example.comПерезапуск:
docker compose -f docker-compose.prod.yml up -d --buildПроверка:
curl -fsS https://api.example.com/healthСертификаты хранятся в томе caddy_data. Для экспериментов с ACME staging можно временно добавить в Caddyfile глобальный блок с acme_ca (не оставляйте в проде без необходимости).
| Значение | Где выполняется LLM | Требования в Compose |
|---|---|---|
http (по умолчанию) |
Внешний API (QWEN_API_URL, HF и т.д.) |
Только сеть и секреты в .env. |
cli |
Бинарь qwen внутри образа mcp-server |
Обычно не используется в slim prod-образе. |
container |
Каждый запрос: docker run --rm образа QWEN_CONTAINER_IMAGE |
Смонтированный docker.sock, собранный qwen-search, QWEN_KNOWLEDGE_HOST_PATH на абсолютный путь к тому же knowledge/, что ./knowledge на хосте. |
Каталог ./knowledge с хоста монтируется в контейнер API как /app/knowledge:ro. Для QWEN_MODE=container переменная QWEN_KNOWLEDGE_HOST_PATH обязательна и должна указывать на тот же каталог на хосте, что вы используете как ./knowledge в compose (например /opt/mcp/knowledge), потому что docker run -v выполняется демоном Docker на хосте.
Пример фрагмента .env для container-режима:
QWEN_MODE=container
QWEN_CONTAINER_IMAGE=spawndock/qwen-search:prod
QWEN_CONTAINER_RUNTIME=docker
QWEN_KNOWLEDGE_HOST_PATH=/srv/spawndock-api/knowledgemcp-server в штатном docker-compose.prod.yml идёт от root (user: "0:0"), чтобы стабильно использовать /var/run/docker.sock. Это даёт контроль над Docker на хосте с точки зрения возможностей процесса API. Ограничьте доступ к публичному API (firewall, TLS, секреты MCP). Вариант усиления — раздел 7.
- Убрать
user: "0:0"уmcp-server, добавить в compose:
group_add:
- "${DOCKER_GID}"На хосте: getent group docker | cut -d: -f3 → в .env: DOCKER_GID=….
- Ограничить доступ к
docker.sockи использовать только read-only монтирование корпуса в образе поиска (текущийqwen-searchуже монтирует корпус:ro).
- Каталог
./knowledgeпримонтирован с хоста: обновите файлы на диске; пересборка образа API для смены текста не обязательна. - При
QWEN_MODE=containerпосле смены пути к знаниям на диске проверьтеQWEN_KNOWLEDGE_HOST_PATHв.env. - После правок
docker-compose.prod.ymlилиCaddyfile:docker compose -f docker-compose.prod.yml up -d --build.
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f mcp-server
docker compose -f docker-compose.prod.yml logs -f bot
docker compose -f docker-compose.prod.yml logs -f caddy
docker images spawndock/qwen-search:prodHealthchecks: в docker compose ps смотрите колонку STATUS (healthy / unhealthy / starting). Если сервис unhealthy, сначала логи этого сервиса, затем детали проверки:
docker inspect "$(docker compose -f docker-compose.prod.yml ps -q mcp-server)" --format '{{json .State.Health}}' | python3 -m json.tool(замените mcp-server на caddy или bot). mcp-server проверяется запросом к GET /health внутри контейнера; bot — доступностью http://mcp-server:3000/health; caddy — HTTP /health через прокси (для PUBLIC_HOST=:80 — на порт из значения, для FQDN — с заголовком Host).
- DEPLOY.md — Hetzner / OpenTofu / cloud-init.
- docker/qwen-search/README.md — образ Qwen CLI.