Проект агента (~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>
113 lines
5.2 KiB
YAML
113 lines
5.2 KiB
YAML
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
|