Проект агента (~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>
imgbot-daemon
Контейнер для фонового демона проекта imgbot — headless-поиска и скачивания
картинок. Проект принадлежит агенту, живёт в /home/tgclaude/imgbot и правится
им же; здесь только среда запуска и присмотр.
Зачем
До этого демон работал как голый процесс на хосте: запущен руками из
SSH-сессии, PPID=1, без юнита и restart-политики, Linger=no у пользователя.
Держался только потому, что в Ubuntu KillUserProcesses=no — после
перезагрузки не поднялся бы, и сигнала об этом не было бы. Плюс его 15%
ядра не были ничем ограничены и не были видны мониторингу.
Контейнер даёт: автозапуск и restart: unless-stopped, потолки CPU/памяти,
видимость в cAdvisor (а значит и в Grafana), и снимает костыль с руками
распакованными библиотеками chromium.
Что тут НЕ запускается и почему
Два других процесса проекта — imgbot-mcp-http (порт 8765) и
imgbot-file-server (8766) — оставлены вне контейнеров сознательно:
- оба жёстко забиндены в коде на
172.22.0.1— адрес шлюза docker-сетиopenclaw_default. В контейнере такой адрес не поднять, а сама привязка ломается при пересоздании сети; - до них всё равно никто не дотягивается: единственный живой потребитель —
агент в контейнере
claude-channels— находится в другой docker-сети и ходит через SSH, а не по HTTP (проверено:curlиз контейнера даёт000); imgbot-mcp-httpвообще без авторизации, в отличие от file-server с его Bearer-токеном.
Их имеет смысл контейнеризовать только вместе с правкой бинда на 0.0.0.0
(две строки: HOST в file_server.py, host= в mcp_server.py) и уже в
общей docker-сети с claude-channels — тогда агент получит MCP по токену
вместо SSH. Это решение владельца кода, не инфраструктуры.
Запуск
Собрать и поднять (на сервере каталог стека — ~/imgbot-daemon/, чтобы не путался с самим проектом в ~tgclaude/imgbot):
docker compose build
# погасить запущенный руками демон, иначе будет два и двойной analyze:
pkill -u tgclaude -f imgbot-daemon
docker compose up -d
docker logs -f imgbot-daemon
Проверка, что расписание пошло: в ~tgclaude/imgbot-data/daemon.log должны
появляться строки _job_analyze ... executed successfully каждые 5 минут и
_job_refresh каждые 30.
Откат
docker compose down
# и вернуть как было, из-под tgclaude:
cd ~/imgbot && .venv/bin/imgbot-daemon >> ~/imgbot-data/daemon.log 2>&1 &
Важно для владельца кода
Перезапуск демона теперь docker restart imgbot-daemon, а не pkill плюс
ручной старт. Если запустить процесс руками при живом контейнере — получатся
два демона, которые будут конкурировать за одни и те же строки SQLite
(в проекте это уже случалось с cron без flock и выглядело как зависание).
Правки кода пересборки не требуют: исходники монтируются read-only, достаточно
docker restart imgbot-daemon. Пересобирать нужно только при изменении
зависимостей в pyproject.toml — и тогда версию playwright в Dockerfile
надо держать совпадающей с uv.lock, иначе разойдётся ревизия браузера.