Операторский чеклист по Docker Compose, QWEN_MODE=container, Docker socket и Caddy (80/443) — в OPERATOR.md. В firewall Hetzner открыты порты 22, 80, 443 (80/443 — публичный HTTP(S) и Let’s Encrypt). Порт 3000 наружу не публикуется: API доступен через Caddy.
Одна VPS на Hetzner (CX22: 2 vCPU, 4 GB RAM, 40 GB диск) с двумя сервисами:
- Caddy (порты 80/443) — reverse proxy на MCP-сервер
- MCP Server (порт 3000 только внутри Docker-сети) — MCP бекенд + control plane + WebSocket tunnel
- Telegram Bot — polling-бот для создания проектов и preview
Qwen-инференс идёт через Hugging Face Inference API (не локально — нет GPU).
# macOS
brew install opentofu
# Linux
curl -fsSL https://get.opentofu.org/install-opentofu.sh | bash
# Windows (через scoop)
scoop install opentofu| Токен | Где взять |
|---|---|
| Hetzner API Token | console.hetzner.cloud → Project → Security → API Tokens → Generate |
| HuggingFace Token | huggingface.co/settings/tokens → New token (Read) |
| Telegram Bot Token | @BotFather → /newbot → скопировать токен |
| Telegram Bot Username | Username бота, например rustgpt_bot |
| Telegram Mini App Short Name | Short name Mini App из BotFather, например test |
# Проверь
ls ~/.ssh/id_ed25519.pub
# Если нет — создай
ssh-keygen -t ed25519cd infra
cp terraform.tfvars.example terraform.tfvarsОтредактируй terraform.tfvars:
hcloud_token = "ВСТАВЬ_HETZNER_API_TOKEN"
hf_token = "hf_ВСТАВЬ_HUGGINGFACE_TOKEN"
ssh_public_key_path = "~/.ssh/id_ed25519.pub"
telegram_bot_token = "123456:ABC-ВСТАВЬ_TELEGRAM_BOT_TOKEN"
telegram_bot_username = "rustgpt_bot"
telegram_mini_app_short_name = "test"
public_origin = "http://PLACEHOLDER"
public_host = "spawn-dock.w3voice.net"Для production по умолчанию используем
https://spawn-dock.w3voice.net.
cd infra
tofu init
tofu validateОжидаемый вывод: Success! The configuration is valid.
tofu planДолжно показать создание 3 ресурсов:
hcloud_ssh_key.defaulthcloud_firewall.mcphcloud_server.mcp
tofu applyНабери yes для подтверждения. Через 1-2 минуты получишь вывод:
server_ip = "1.2.3.4"
ssh_command = "ssh root@1.2.3.4"
mcp_url = "https://spawn-dock.w3voice.net"
Отредактируй terraform.tfvars:
public_origin = "https://spawn-dock.w3voice.net"
public_host = "spawn-dock.w3voice.net"Если домен ещё не указывает на VPS, сначала обнови DNS A/AAAA записи. После этого примени изменение:
tofu applyЭто пересоздаст VPS с правильным
PUBLIC_ORIGINи доменнымPUBLIC_HOST. Всё автоматически.
Cloud-init устанавливает Docker, клонирует репо, собирает и запускает контейнеры. Проверь:
# Подключись к серверу
ssh root@1.2.3.4
# Проверь статус cloud-init
cloud-init status --wait
# Проверь контейнеры
cd /opt/mcp && docker compose -f docker-compose.prod.yml ps
# Логи MCP сервера
docker compose -f docker-compose.prod.yml logs mcp-server
# Логи бота
docker compose -f docker-compose.prod.yml logs bot# Health check
curl https://spawn-dock.w3voice.net/health
# Ожидаемо: {"status":"ok"}
# Проверь бота — отправь /help в Telegramtofu apply
↓
Hetzner создаёт VPS (Ubuntu 24.04)
↓
cloud-init:
1. apt install docker
2. git clone SpawnDock/MCP → /opt/mcp
3. Создаёт .env с токенами
4. docker compose up --build
├── caddy (80/443 → mcp-server:3000)
├── mcp-server (внутренний :3000)
└── bot (polling Telegram)
5. Smoke test: curl localhost/health
┌─────────────────────────────────────┐
│ Hetzner CX22 VPS │
│ │
│ ┌───────────────────────────────┐ │
│ │ Docker Compose │ │
│ │ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ caddy (:80, :443) │ │ │
│ │ └───────────┬─────────────┘ │ │
│ │ │ reverse_proxy │ │
│ │ ┌───────────▼─────────────┐ │ │
│ │ │ mcp-server (:3000) │ │ │
│ │ │ ├─ MCP бекенд (/sse) │ │ │
│ │ │ ├─ Control Plane API │ │ │
│ │ │ ├─ WebSocket Tunnel │ │ │
│ │ │ └─ Preview Proxy │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ bot │ │ │
│ │ │ └─ Telegram polling │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
│ │
│ Firewall: 22 (SSH), 80 (HTTP), 443 (HTTPS) │
└─────────────────────────────────────┘
│
│ Qwen запросы
↓
HuggingFace Inference API
# Подключиться к серверу
ssh root@$(cd infra && tofu output -raw server_ip)
# Перезапустить сервисы
ssh root@IP "cd /opt/mcp && docker compose -f docker-compose.prod.yml restart"
# Обновить код на сервере (без пересоздания VPS)
ssh root@IP "cd /opt/mcp && git pull && docker compose -f docker-compose.prod.yml up -d --build"
# Посмотреть логи в реальном времени
ssh root@IP "cd /opt/mcp && docker compose -f docker-compose.prod.yml logs -f"
# Проверить состояние проектов
ssh root@IP "cat /opt/mcp/data/state/state.json | python3 -m json.tool"cd infra
tofu destroyНабери yes. VPS и все ресурсы Hetzner будут удалены. Ничего не останется.
ssh root@IP
cat /var/log/cloud-init-output.log | tail -50ssh root@IP
cd /opt/mcp
docker compose -f docker-compose.prod.yml logs# Проверь что TELEGRAM_* переменные и short name на месте
ssh root@IP "grep TELEGRAM /opt/mcp/.env"
# Проверь логи бота
ssh root@IP "cd /opt/mcp && docker compose -f docker-compose.prod.yml logs bot"# Проверь что сервер отвечает (порт 80, Caddy)
curl http://IP/health
# Проверь SSE endpoint
curl -N http://IP/sse| Переменная | Описание | Значение по умолчанию |
|---|---|---|
PORT |
Порт сервера | 3000 |
QWEN_API_URL |
URL Qwen/HF API | http://localhost:8080 |
HF_TOKEN |
HuggingFace токен | — |
QWEN_MODEL |
Модель для инференса | qwen3-coder |
QWEN_TIMEOUT_MS |
Таймаут запроса к Qwen | 60000 |
RATE_LIMIT_RPS |
Лимит запросов в секунду | 10 |
PUBLIC_HOST |
Адрес Caddy (:80 или FQDN для HTTPS) |
:80 |
PUBLIC_ORIGIN |
Публичный URL сервера (как у клиентов) | http://localhost |
TELEGRAM_BOT_TOKEN |
Токен Telegram бота | — |
TELEGRAM_BOT_USERNAME |
Username бота для deep link | — |
TELEGRAM_MINI_APP_SHORT_NAME |
Short name Mini App для deep link | — |
STATE_FILE |
Путь к файлу состояния | .spawndock/state.json |
TUNNEL_PATH |
Путь WebSocket tunnel | /tunnel/connect |
PREVIEW_PREFIX |
Префикс preview URL | /preview |
REQUEST_TIMEOUT_MS |
Таймаут tunnel proxy | 15000 |
CONTROL_PLANE_URL |
URL control plane (для бота) | http://localhost:3000 |