Автопубликация статей через cron, Claude Code и Docker: pipeline без ручной рутины
Когда у блога два-три поста в месяц — всё просто. Сел вечером, написал, запушил. Когда хочется выпускать пост каждые два-три дня и при этом не превратить блог во вторую работу — начинаются вопросы. Кто пишет? Когда пишет? Кто проверяет, что пост вообще годится к публикации? И главное — кто следит, чтобы это всё не сломалось в три часа ночи в субботу?
Этот пост — про реальный пайплайн, который сейчас крутится у меня и публикует статьи по контент-плану без моего ежедневного участия. По кругу: cron будит контейнер, headless Claude Code CLI пишет пост по правилам, guard-check валидирует результат, git push отправляет на GitHub, а dev-сервер за reverse-proxy подхватывает изменения. Разберём по шагам — что, зачем, и где это любит ломаться.
Зачем вообще автопубликация: проблема ручного релиза
Ручной релиз поста — это не «написать текст». Это:
- открыть редактор, вспомнить шаблон frontmatter,
- проверить, что slug не повторяется,
- нарисовать диаграмму и убедиться, что Mermaid её проглотит,
- расставить внутренние ссылки на старые посты,
- закоммитить, запушить, дождаться билда, открыть прод и убедиться, что не сломалось.
На один пост — час-полтора, если повезло. На десять постов в месяц — это уже отдельная работа, причём из тех, что прокрастинируется первой.
Автоматизация решает не «как писать быстрее», а как не забросить блог через два месяца. Машина не устаёт и не пропускает четверг.
Архитектура пайплайна: что и куда зовёт
Сначала картинка целиком, потом по слоям.
На диаграмме четыре зоны ответственности. Синяя — инфраструктура (cron, entrypoint, webhook, Docker). Жёлтая — guards и валидация: тут принимаются решения «можно/нельзя». Зелёная — собственно генерация и git-операции. Фиолетовая — данные и состояние: правила в postRule.md и удалённый репозиторий.
Поток одной публикации
- Cron внутри контейнера срабатывает по расписанию (например,
0 9 */2 * *— каждые два дня в 9 утра). - Entrypoint загружает env, активирует ключи и зовёт guard-check.
- Guard-check смотрит: не превышен ли месячный лимит постов, есть ли свободные темы в плане, не релизный ли «тихий день». Если что-то не так — тихо выходит. Это важная штука, без неё бот легко выпустит десять постов за неделю и испортит SEO.
- Claude Code CLI запускается в headless-режиме с фиксированным промптом: «прочитай
postRule.md, возьми первую тему со статусом нет, создай пост, обнови таблицу». - Lint проверяет MDX-синтаксис, валидность frontmatter, корректность Mermaid.
- Если что-то сломалось — фидбек уходит обратно в CLI и тот переписывает только проблемную часть. Не более 2–3 итераций, иначе бесконечный цикл при сломанном API.
- Git коммитит и пушит на GitHub.
- Webhook или периодический pull на проде стягивает изменения, Docker Compose пересобирает контейнер с Astro, и статика уезжает за nginx читателю.
Cron внутри Docker: главная грабля
Тут есть ловушка, на которую наступает примерно каждый. Cron в чистом python:3.12-slim или node:20-alpine не работает из коробки. Образ не запускает crond сам, а если и запускает — он не видит переменных окружения, которые вы передали через docker run -e ....
Решений два:
- Запускать cron как PID 1, прокидывая env через файл (
/etc/environmentили ручной экспорт в crontab перед командой). - Использовать внешний планировщик — systemd timer на хосте, GitHub Actions schedule, или отдельный sidecar-контейнер с
ofelia/mcuadros/ofelia.
Я выбрал первый вариант для автономности (хост может вообще ничего не знать про блог), но вариант с GitHub Actions честно проще, если вы уже на GitHub.
Минимальный Dockerfile с cron
FROM node:20-bookworm-slim
RUN apt-get update && apt-get install -y cron git ca-certificates curl \ && rm -rf /var/lib/apt/lists/*
WORKDIR /appCOPY . .RUN npm ci
COPY crontab /etc/cron.d/blog-cronRUN chmod 0644 /etc/cron.d/blog-cron && crontab /etc/cron.d/blog-cron
COPY entrypoint.sh /entrypoint.shRUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]crontab файл:
SHELL=/bin/bash0 9 */2 * * root /app/entrypoint.sh >> /var/log/blog.log 2>&1entrypoint.sh (упрощённо):
#!/usr/bin/env bashset -euo pipefail
env | grep -E '^(ANTHROPIC|GITHUB|BLOG)_' > /etc/environmentservice cron starttail -f /var/log/blog.logЗачем tail -f в конце — чтобы контейнер не схлопывался. Cron живёт в фоне, основной процесс должен висеть в foreground, иначе Docker считает контейнер «упавшим» и рестартует его в цикле.
Headless Claude Code: как писать постом из скрипта
Claude Code умеет работать без интерактивного UI. Передаёте промпт в --print режим и читаете результат. Для генерации поста это выглядит так:
claude --print --permission-mode acceptEdits \ --append-system-prompt "$(cat postRule.md)" \ "Создай новый пост по правилам в postRule.md. \ Возьми ПЕРВУЮ тему со статусом нет. \ После создания обнови таблицу в postRule.md."Несколько важных деталей:
--permission-mode acceptEditsнужен потому, что в headless-режиме некому жать «yes» на каждый Edit. Без этого CLI замрёт в ожидании пользователя.- Системный промпт через
--append-system-prompt— единственный способ дать модели большой контекст с правилами без раздувания обычного prompt-поля. - Лучше не давать доступ к git внутри одного и того же запуска. Пусть Claude пишет файлы, а коммитит уже отдельный shell-скрипт. Так проще делать guard-check между генерацией и push.
Guard-check: то, что спасает от стыда
Между «Claude дописал файл» и «git push» обязательно должен стоять валидатор. Минимальный набор проверок:
| Проверка | Что ловит |
|---|---|
| Файл существует и непустой | Случай, когда CLI упал на половине |
| Frontmatter парсится | Сломанный YAML, отсутствующий slug или date |
| Длина поста ≥ N строк | «Поверхностный» пост, написанный второпях |
| Mermaid-блоки синтаксически валидны | Падение билда Astro |
Slug не повторяется в src/content/blog/ | Перезатирание старого поста |
| Внутренние ссылки ведут на существующие slug-и | 404 в навигации |
Если хоть одна проверка падает — git push не происходит. Файл остаётся в репозитории как «черновик от бота», и при следующем запуске можно либо переписать, либо вынести наружу для ручной правки. Главное — не пушить сломанное.
Mermaid внутри MDX: где обычно ломается билд
Astro с MDX и кастомным компонентом <Mermaid /> — гибкая связка, но капризная. Самые частые поломки:
- Кавычки в подписях узлов. Внутри
["..."]нельзя ставить двоеточие или круглые скобки без экранирования — парсер Mermaid принимает их за синтаксис. - Кириллица в id узлов.
узел1 --> узел2иногда работает, иногда нет в зависимости от версии. Безопаснее держать id латиницей, а подписи — на русском внутри кавычек. - Длинные subgraph без переводов строк. Парсер падает молча, диаграмма просто не рисуется.
Поэтому в guard-check полезно прогонять каждый Mermaid-блок через отдельный валидатор — например, через MCP-сервер mermaid-mcp с инструментом validate_and_render_mermaid_diagram. Если в проекте используется Model Context Protocol — это естественное место, где он окупается.
Деплой: GitHub как триггер, Docker как раннер
После того как файл прошёл валидацию и попал в master, остаётся доставить статику на прод. Два рабочих варианта:
- Webhook на сервере. GitHub дёргает endpoint вашего сервиса при каждом push. Сервис проверяет подпись, делает
git pull, потомdocker compose build && docker compose up -d. Быстро, но требует открытого порта и обработки secret. - Pull-модель. На сервере отдельный cron каждые 5 минут делает
git fetchи сравнивает HEAD. Если разошлось — пересобирает. Медленнее, но не нужно ничего открывать наружу.
Оба варианта про инфраструктурные решения, и оба упираются в нюансы reverse-proxy и заголовков. Если у вас Cloudflare перед сайтом — рекомендую заранее почитать про CORS, заголовки и совместимость с upstream-провайдерами, иначе сюрпризы найдут вас сами.
Pull-модель в одном docker-compose.yml
services: blog: build: . restart: unless-stopped environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} volumes: - ./content:/app/content - ./logs:/var/log watcher: image: alpine/git command: > sh -c "while true; do cd /repo && git fetch && git reset --hard origin/master; sleep 300; done" volumes: - ./repo:/repoТут watcher просто приводит локальный клон к состоянию origin/master. Сборка статики и рестарт сервиса — отдельной командой по событию (например, через inotifywait на изменение файла .commit-marker). Это не серебряная пуля, но для блога одного автора более чем достаточно.
Что важно помнить, прежде чем включать всё это в прод
- Поставьте лимит на количество запусков. Один баг в guard-check — и за ночь у вас 30 одинаковых постов. У меня стоит «не более одного поста за 36 часов», даже если cron сработает чаще.
- Логируйте всё. Каждый запуск, каждый Claude-вызов, каждый push. Когда через два месяца что-то сломается, читать логи будет приятнее, чем гадать.
- Держите kill-switch. Простой файл
.pauseв репозитории, наличие которого блокирует любую публикацию. Когда едете в отпуск или редизайните блог —touch .pauseи спите спокойно. - Ревьюйте раз в неделю. Машина пишет неплохо, но дрейфует. Раз в неделю прочитать последние посты — это десять минут, которые экономят репутацию.
Заключение
Автопубликация — это не про то, чтобы заменить себя как автора. Это про то, чтобы убрать рутину между «у меня есть мысль» и «эта мысль доехала до читателя». Cron, Claude Code, guard-check, Docker — каждый кусок по отдельности банален, но вместе они дают то, чего ручной процесс не даёт никогда: предсказуемость.
PS: если ваш бот в три часа ночи в субботу выпустил пост со сломанным Mermaid и сайт лежит — поздравляю, вы только что выяснили, что guard-check был недостаточно жёстким. У всех нас был такой пост. Главное — что следующий уже не сломается.