Logo
Overview

Автопубликация статей через cron, Claude Code и Docker: pipeline без ручной рутины

May 15, 2026
8 min read

Автопубликация статей через cron, Claude Code и Docker: pipeline без ручной рутины

Когда у блога два-три поста в месяц — всё просто. Сел вечером, написал, запушил. Когда хочется выпускать пост каждые два-три дня и при этом не превратить блог во вторую работу — начинаются вопросы. Кто пишет? Когда пишет? Кто проверяет, что пост вообще годится к публикации? И главное — кто следит, чтобы это всё не сломалось в три часа ночи в субботу?

Этот пост — про реальный пайплайн, который сейчас крутится у меня и публикует статьи по контент-плану без моего ежедневного участия. По кругу: cron будит контейнер, headless Claude Code CLI пишет пост по правилам, guard-check валидирует результат, git push отправляет на GitHub, а dev-сервер за reverse-proxy подхватывает изменения. Разберём по шагам — что, зачем, и где это любит ломаться.

Зачем вообще автопубликация: проблема ручного релиза

Ручной релиз поста — это не «написать текст». Это:

  • открыть редактор, вспомнить шаблон frontmatter,
  • проверить, что slug не повторяется,
  • нарисовать диаграмму и убедиться, что Mermaid её проглотит,
  • расставить внутренние ссылки на старые посты,
  • закоммитить, запушить, дождаться билда, открыть прод и убедиться, что не сломалось.

На один пост — час-полтора, если повезло. На десять постов в месяц — это уже отдельная работа, причём из тех, что прокрастинируется первой.

Автоматизация решает не «как писать быстрее», а как не забросить блог через два месяца. Машина не устаёт и не пропускает четверг.

Архитектура пайплайна: что и куда зовёт

Сначала картинка целиком, потом по слоям.

100%
graph TD
  CRON["cron в контейнере"] --> ENTRY["entrypoint скрипт"]
  ENTRY --> CHECK["guard-check: лимиты, день недели, очередь"]
  CHECK -->|"можно публиковать"| CC["Claude Code CLI headless"]
  CHECK -->|"стоп"| EXIT["выход без действий"]
  CC --> RULES["postRule.md план и правила"]
  CC --> WRITE["новый файл index.mdx"]
  WRITE --> LINT["проверка mdx и mermaid"]
  LINT -->|"ок"| GIT["git add commit push"]
  LINT -->|"ошибка"| FIX["повтор Claude Code с фидбеком"]
  FIX --> WRITE
  GIT --> GH["GitHub репозиторий"]
  GH --> WEBHOOK["webhook или pull на сервере"]
  WEBHOOK --> DOCKER["docker compose pull build up -d"]
  DOCKER --> SITE["прод: Astro статика за nginx"]
  SITE --> READER["читатель блога"]

  style CRON fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style ENTRY fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style CHECK fill:#f0a500,stroke:#c88400,color:#fff
  style CC fill:#50c878,stroke:#3a9a5c,color:#fff
  style RULES fill:#7b68ee,stroke:#5a4db2,color:#fff
  style WRITE fill:#50c878,stroke:#3a9a5c,color:#fff
  style LINT fill:#f0a500,stroke:#c88400,color:#fff
  style FIX fill:#f0a500,stroke:#c88400,color:#fff
  style GIT fill:#50c878,stroke:#3a9a5c,color:#fff
  style GH fill:#7b68ee,stroke:#5a4db2,color:#fff
  style WEBHOOK fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style DOCKER fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style SITE fill:#50c878,stroke:#3a9a5c,color:#fff
  style READER fill:#e0e0e0,stroke:#999,color:#000
  style EXIT fill:#e0e0e0,stroke:#999,color:#000

На диаграмме четыре зоны ответственности. Синяя — инфраструктура (cron, entrypoint, webhook, Docker). Жёлтая — guards и валидация: тут принимаются решения «можно/нельзя». Зелёная — собственно генерация и git-операции. Фиолетовая — данные и состояние: правила в postRule.md и удалённый репозиторий.

Поток одной публикации

  1. Cron внутри контейнера срабатывает по расписанию (например, 0 9 */2 * * — каждые два дня в 9 утра).
  2. Entrypoint загружает env, активирует ключи и зовёт guard-check.
  3. Guard-check смотрит: не превышен ли месячный лимит постов, есть ли свободные темы в плане, не релизный ли «тихий день». Если что-то не так — тихо выходит. Это важная штука, без неё бот легко выпустит десять постов за неделю и испортит SEO.
  4. Claude Code CLI запускается в headless-режиме с фиксированным промптом: «прочитай postRule.md, возьми первую тему со статусом нет, создай пост, обнови таблицу».
  5. Lint проверяет MDX-синтаксис, валидность frontmatter, корректность Mermaid.
  6. Если что-то сломалось — фидбек уходит обратно в CLI и тот переписывает только проблемную часть. Не более 2–3 итераций, иначе бесконечный цикл при сломанном API.
  7. Git коммитит и пушит на GitHub.
  8. 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 /app
COPY . .
RUN npm ci
COPY crontab /etc/cron.d/blog-cron
RUN chmod 0644 /etc/cron.d/blog-cron && crontab /etc/cron.d/blog-cron
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]

crontab файл:

SHELL=/bin/bash
0 9 */2 * * root /app/entrypoint.sh >> /var/log/blog.log 2>&1

entrypoint.sh (упрощённо):

#!/usr/bin/env bash
set -euo pipefail
env | grep -E '^(ANTHROPIC|GITHUB|BLOG)_' > /etc/environment
service cron start
tail -f /var/log/blog.log

Зачем tail -f в конце — чтобы контейнер не схлопывался. Cron живёт в фоне, основной процесс должен висеть в foreground, иначе Docker считает контейнер «упавшим» и рестартует его в цикле.

Headless Claude Code: как писать постом из скрипта

Claude Code умеет работать без интерактивного UI. Передаёте промпт в --print режим и читаете результат. Для генерации поста это выглядит так:

Terminal window
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 был недостаточно жёстким. У всех нас был такой пост. Главное — что следующий уже не сломается.