Files
infra/stacks/imgbot/docker-compose.yml
T
gilfanovrandClaude Opus 5 a65d6a249e imgbot: стек из двух сервисов вместо процессов на хосте
Проект агента (~tgclaude/imgbot) работал тремя голыми python-процессами,
запущенными руками из SSH-сессии: без юнита, без restart-политики, при
Linger=no. Держались только на KillUserProcesses=no и не пережили бы
перезагрузку, причём молча.

Один образ на базе playwright/python:v1.62.0-noble (версия пакета
playwright обязана совпадать с тегом, иначе разъезжается ревизия
chromium), два сервиса:
  - imgbot-daemon — APScheduler: refresh 30 мин, analyze 5 мин
  - imgbot-http   — объединил прежние mcp-http (8765) и file-server
    (8766): MCP как под-приложение Starlette плюс маршрут /photo/<id>

Неочевидное, из-за чего это ломается молча (в compose есть комментарии):
  - данные монтируются в тот же путь, что на хосте: images.file_path в
    SQLite абсолютный, и его читают обе стороны
  - корней хранения может быть больше одного — проверять все префиксы
    file_path, а не только data_dir
  - HOME обязателен: collector.py вызывает Path.home(), а uid 1001 не
    имеет записи в /etc/passwd образа
  - у MCP своя защита от DNS-rebinding: без IMGBOT_ALLOWED_HOSTS отдаёт
    421 на любой Host кроме localhost

Порт не публикуется на хост, сервис доступен из общей docker-сети с
claude-channels — агент наконец ходит по MCP, а не по SSH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:26:16 +03:00

113 lines
5.2 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: imgbot
services:
imgbot-daemon:
build: .
image: imgbot-daemon:local
container_name: imgbot-daemon
restart: unless-stopped
# uid:gid пользователя tgclaude: .env лежит с mode 600, данные принадлежат
# ему же. Под другим uid контейнер не прочитает ключи и не запишет картинки.
user: "1001:1001"
volumes:
# Исходники read-only: правки агента применяются `docker restart`.
- /home/tgclaude/imgbot:/app:ro
# Данные: images/, manifest.db, куки google_state/reddit_state, логи.
# ВАЖНО: путь внутри контейнера обязан совпадать с путём на хосте.
# И проверять надо ВСЕ префиксы file_path в манифесте, а не только этот
# (был второй корень — легаси-архив ~tgclaude/images, снесён 2026-09-08):
# select substr(file_path,1,28), count(*) from images group by 1;
# В SQLite `images.file_path` лежит абсолютным путём хоста, и его читают
# как этот контейнер, так и оставшийся на хосте imgbot-file-server.
# Любое другое место монтирования ломает обе стороны: analyze падает с
# FileNotFoundError на старых строках, а новые строки становятся
# нечитаемыми для хостовых процессов.
- /home/tgclaude/imgbot-data:/home/tgclaude/imgbot-data
# Таймзона хоста: без этого контейнер пишет в общий daemon.log в UTC,
# а строки от прежнего хостового запуска — в CEST, и лог мешает два пояса.
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
IMGBOT_DATA_DIR: /home/tgclaude/imgbot-data
PYTHONPATH: /app/src
HOME: /tmp
# .env подхватывается самим приложением: config.py ищет его как
# parents[2]/.env относительно пакета, то есть /app/.env через монтирование.
# Через env_file не пробрасываем, чтобы ключи не размножать.
# chromium падает на дефолтных 64 МБ /dev/shm
shm_size: 1gb
# Демон ровно жрёт ~15% ядра постоянно (analyze каждые 5 мин, 3 vision-запроса
# на картинку). Потолок — чтобы refresh с playwright не разогнался на все 8.
cpus: "2.0"
mem_limit: 3g
# Лог остаётся там, где его читает агент, и дублируется в `docker logs`.
command: >
sh -c 'python3 -c "from imgbot.daemon import main; main()" 2>&1 | tee -a /home/tgclaude/imgbot-data/daemon.log'
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Объединённый HTTP-сервис: MCP (/mcp) и раздача картинок (/photo/<id>) в
# одном процессе. Раньше это были два процесса на двух портах, оба
# захардкоженные на 172.22.0.1 — адрес шлюза docker-сети openclaw_default.
imgbot-http:
build: .
image: imgbot-daemon:local
container_name: imgbot-http
restart: unless-stopped
user: "1001:1001"
volumes:
- /home/tgclaude/imgbot:/app:ro
- /home/tgclaude/imgbot-data:/home/tgclaude/imgbot-data
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
IMGBOT_DATA_DIR: /home/tgclaude/imgbot-data
PYTHONPATH: /app/src
HOME: /tmp
# URL, который сервис выдаёт в image_url: имя контейнера в общей
# docker-сети с claude-channels, откуда его и качает агент.
IMGBOT_PUBLIC_BASE: http://imgbot-http:8765
# Порт НЕ публикуется на хост: сервис доступен только из docker-сетей,
# к которым подключён контейнер (и с самой машины). Авторизации у него
# нет сознательно — см. README проекта.
expose:
- "8765"
networks:
- default
- claude-channels
# request_photos гоняет playwright внутри этого же процесса, поэтому
# лимиты и shm такие же, как у демона.
shm_size: 1gb
cpus: "2.0"
mem_limit: 3g
command: python3 -c "from imgbot.app import main; main()"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
# Сеть проекта claude-channels: агент внутри того контейнера обращается к
# imgbot по имени imgbot-http. Тот же приём, что у gitea с monitoring.
claude-channels:
external: true
name: claude-channels_default