База знаний

Directus — headless CMS и backend как контентный хаб для мультисайтовой архитектуры

Directus превращает любую SQL-базу в общий бэкенд для людей и ИИ-агентов: единый админ-интерфейс, мгновенные REST и GraphQL API, политики доступа, нативный MCP-сервер и простое развёртывание на собственном VPS.

Опубликовано

Когда вокруг вас уже есть пара сайтов, отдельная база знаний и ИИ-агенты, которые должны где-то читать и куда-то писать — очень быстро становится понятно, что им всем нужен один общий бэкенд. Не три плагина к WordPress, не пять копий контента в Notion, а одно место, где лежит правда.

Directus (directus.io) — это open-source headless CMS и backend-as-a-service, который надстраивается поверх вашей SQL-базы и превращает её в такой общий контур. В этом руководстве разберу, как он устроен, зачем нужен для мультисайтовой архитектуры и ИИ-агентов и как развернуть его на своём VPS — даже если терминал для вас пока не родной.


Что такое Directus

Directus — это бесплатная open-source платформа, которая подключается к вашей SQL-базе (PostgreSQL, MySQL, SQLite, MS SQL, OracleDB) и достраивает поверх неё четыре вещи:

  • Админку. Удобный веб-интерфейс для редакторов, где можно работать с любыми таблицами как с контент-базой.
  • Мгновенные API. REST и GraphQL появляются автоматически — ничего писать не надо.
  • Права и политики. Кто что видит, кто что правит, вплоть до отдельных строк и полей.
  • Автоматизации и ИИ. Встроенный no-code конструктор Flows и нативный MCP-сервер для агентов.

Главное отличие от классических CMS — Directus не навязывает свою закрытую схему. База данных остаётся вашей, и в любой момент вы можете его отключить и продолжить работать напрямую с PostgreSQL. Это ровно та архитектура, которую я называю «внешний контур»: данные живут в независимом слое, а не внутри конкретного инструмента.

💡
Коротко. Если Strapi и Sanity — это в первую очередь headless CMS, то Directus — это CMS и общий backend сразу. Отсюда и сценарии: мультисайт, базы знаний и хранилища для ИИ-агентов закрываются одним инструментом.

Почему именно контентный хаб, а не очередная CMS

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

Контентный хаб решает эту задачу одним движением: один бэкенд, одна база, много витрин.

Один Directus — несколько сайтов

В моей архитектуре один экземпляр Directus может обслуживать сразу:

  • pimenov.ai на Astro
  • pimenov.ru на Astro или Nuxt
  • reload.pimenov.ai — закрытую платформу клуба
  • внутренние базы знаний и справочники
  • хранилища данных для ИИ-агентов и Telegram-ботов

Каждая витрина ходит в один и тот же API, но видит только свой срез контента — за это отвечают политики доступа, а не отдельные инсталляции.

Один источник для людей и для агентов

Notion image
flowchart LR
	DB[(PostgreSQL)] --> D[Directus]
	D -->|REST / GraphQL| S1["pimenov.ai"]
	D -->|REST / GraphQL| S2["reload.pimenov.ai"]
	D -->|REST / GraphQL| S3["pimenov.ru"]
	D -->|MCP| A1["ИИ-агенты контента"]
	D -->|Flows| A2["Автоматизации"]
	D -->|App| U["Редакторы"]

Фронтенд, редактор и агент читают и пишут в одну и ту же базу через один и тот же слой правил. Никакой дублирующей логики, никаких разных источников правды.


Как это выглядит против Strapi и Sanity

Практичное сравнением — на что смотреть, если выбираете headless CMS именно под сценарий «много сайтов и агенты».

Что сравниваемDirectusStrapiSanity
База данныхЛюбая существующая SQLСвоя SQL-схемаПроприетарный облачный datastore
Self-hosted на своём VPSДа, один Docker-образДаНет, только Cloud
Админка «из коробки»Полная, без кодаПолная, с настройкойStudio, требует JS-кода
Нативный MCP для ИИДаНетНет
Встроенные автоматизацииFlowsЧерез внешние сервисыЧерез Functions
Глубина политикКоллекция + поле + строкаКоллекция + полеDataset + роль
⚖️
Честный trade-off. Sanity удобнее, если у вас чистый контент и важна скорость редактирования. Strapi ближе к классической Node-разработке. Directus выигрывает там, где нужен общий backend для данных и контента одновременно и прямой доступ ИИ-агентов к нему без прослоек.

Архитектура контентного хаба

В простом виде всё выглядит так: один VPS, один PostgreSQL, один Directus и несколько логических зон внутри.

flowchart TB
	subgraph VPS["VPS (Docker Compose)"]
		subgraph DB["PostgreSQL"]
			C1["Зона Sites"]
			C2["Зона Knowledge"]
			C3["Зона Agents"]
		end
		D["Directus"]
		R["Redis (кэш)"]
		N["Caddy / Nginx"]
	end
	D --- DB
	D --- R
	N --- D
	N -->|HTTPS| Web["Сайты и ИИ-агенты"]

И четыре логических зоны, которые удобно держать в голове с самого начала:

ЗонаЧто хранитКто пишетКто читает
SitesСтраницы, статьи, блоки, медиа сайтовРедакторы, ИИ-ассистентФронтенды, агенты
KnowledgeСправочники, методички, канонические документыВы, редакторыИИ-агенты, чат-боты
AgentsПромпты, логи, задачи и результаты агентовАгенты, оркестраторВы, другие агенты
MediaФайлы, изображения, аудио, PDFРедакторы, агентыВсе
📌
Привязку контента к сайту делайте через отдельную коллекцию sites и поле-связь на неё, а не через префикс в названии. Так одна политика закрывает весь контент одного сайта от другого и от агентов, которым туда нельзя.

Права и политики: зачем это важно для агентов

В Directus используется policy-based access control — политика это набор правил, который прикрепляется к роли или напрямую к пользователю.

Для контентного хаба минимально достаточно пяти ролей:

РольЧто можетКому
AdminВсёВам
EditorCRUD по контенту своего сайтаРедакторам
Agent-WriterСоздавать черновики статей, читать базу знанийАгентам-писателям
Agent-ReaderТолько чтение базы знаний и сайтовRAG-ботам и чат-агентам
PublicЧтение опубликованного контентаФронтендам

Правила умеют учитывать значения в строках. Например, редактору pimenov.ai можно разрешить править статьи только своего сайта:

{
	"collection": "articles",
	"action": "update",
	"permissions": { "site": { "_eq": "pimenov.ai" } },
	"fields": ["title", "body", "status", "tags"]
}

И отдельный важный момент для ИИ-агентов — никогда не давайте одного токена на всех. Для каждого агента заводите отдельного пользователя с узкой ролью и своим статическим токеном. Это даёт три вещи:

  • Раздельные логи действий в directus_activity
  • Возможность моментально отозвать доступ одному конкретному агенту
  • Понятную историю: кто из агентов что и когда изменил

API и интеграция

REST

Каждая коллекция автоматически получает REST-эндпоинт:

# Опубликованные статьи для pimenov.ai
curl "https://concms.pimenov.ai/items/articles?filter[site][_eq]=pimenov.ai&filter[status][_eq]=published&fields=title,slug,body,tags.*" \
	-H "Authorization: Bearer $DIRECTUS_TOKEN"

GraphQL

То же самое через GraphQL — удобно, когда нужно забирать связанные сущности одним запросом:

query PublishedArticles {
	articles(
		filter: { site: { _eq: "pimenov.ai" }, status: { _eq: "published" } }
	) {
		title
		slug
		body
		tags { name }
	}
}

Astro Content Layer

Для Astro-сайта (как pimenov.ai) Directus подключается как источник через официальный loader:

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { directusLoader } from '@directus/astro';

export const collections = {
	articles: defineCollection({
		loader: directusLoader({
			url: import.meta.env.DIRECTUS_URL,
			token: import.meta.env.DIRECTUS_TOKEN,
			collection: 'articles',
			filter: { site: { _eq: 'pimenov.ai' }, status: { _eq: 'published' } },
		}),
	}),
};

На каждый сайт — свой loader с нужным фильтром. База одна, фильтры разные.


Внутренние ИИ-агенты и ассистенты

Это, пожалуй, самая интересная часть истории. Directus предлагает три уровня работы с ИИ, и их можно спокойно использовать вместе.

AI Assistant прямо в админке

Встроенный помощник в интерфейсе Directus. Умеет писать и переписывать текст в полях, переводить контент между языками, суммировать длинные документы и выполнять простые действия по запросу редактора.

Это инструмент для тех, кто работает руками в админке и хочет быстрого ассистента «под рукой» — без отдельного чат-окна и копи-пасты.

📌
AI Assistant наследует права вызывающего пользователя. Редактор pimenov.ai физически не сможет через ассистента отредактировать материал pimenov.ru — политики закрывают это раньше, чем до модели дойдёт запрос.

Native MCP Server

Дальше становится интереснее. Directus работает как нативный MCP-сервер — это значит, что любой MCP-клиент (Claude Desktop, Cursor, ваши собственные агенты на OpenAI или Anthropic SDK) может подключиться к нему и получить набор инструментов: чтение, запись, поиск, работа с файлами.

Типичный конвейер для контент-агента:

  1. Агент «Писатель» подключается к Directus через MCP с токеном роли Agent-Writer
  2. Читает базу знаний и предыдущие публикации по теме
  3. Создаёт черновик статьи со статусом draft и автором = токен агента
  4. Редактор видит черновик в админке, правит, меняет статус на published
  5. Фронтенд (Astro ISR) подхватывает опубликованную статью и ребилдит страницу

Подключение MCP-клиента на примере Claude Desktop:

{
	"mcpServers": {
		"directus": {
			"command": "npx",
			"args": ["-y", "@directus/mcp"],
			"env": {
				"DIRECTUS_URL": "https://cms.pimenov.ai",
				"DIRECTUS_TOKEN": "<токен роли Agent-Writer>"
			}
		}
	}
}

Flows — короткие автоматизации как «внутренние агенты»

Flows — встроенный конструктор потоков с триггерами и операциями. Триггеры: изменение данных, расписание, webhook, ручной запуск. Операции: условия, работа с коллекциями, HTTP-запросы, скрипты и вызовы LLM через расширения.

Что удобно собирать на Flows:

  • Авто-теггер. При создании статьи LLM расставляет теги по тексту.
  • Перевод. При смене языка карточки запускается перевод, результат пишется в соседнее поле.
  • Индексация базы знаний. При обновлении документа автоматически пересчитываются чанки и эмбеддинги в knowledge_chunks (pgvector).
  • Уведомления. При переводе статьи в статус review бот пишет автору в Telegram.

Grani простая: Flows — для коротких реактивных автоматизаций внутри Directus. Длинные многошаговые сценарии — уже задача внешнего оркестратора, например OpenClaw. Обе системы ходят в одну и ту же базу, но с разными токенами и ролями.


Развёртывание на своём VPS — по шагам

Дальше — практика. Я описываю её так, чтобы можно было пройти по шагам, даже если терминал для вас пока не родной. Все команды выполняются через SSH к вашему VPS: ssh user@ip-адрес-vps.

Что понадобится

  • VPS с Ubuntu 22.04+, минимум 2 vCPU / 4 ГБ RAM / 40 ГБ SSD
  • Домен (например, cms.pimenov.ai) с A-записью на IP VPS
  • Docker и Docker Compose
  • Caddy или Nginx перед Directus для HTTPS

Шаг 1. Поставить Docker

# Обновляем список пакетов
sudo apt update

# Ставим Docker и плагин Compose
sudo apt install -y docker.io docker-compose-plugin

# Разрешаем текущему пользователю запускать docker без sudo
sudo usermod -aG docker $USER

# Выходим, чтобы изменения применились
exit

После повторного ssh проверяем:

docker --version
docker compose version

Шаг 2. Создать структуру папок

mkdir -p ~/directus && cd ~/directus
mkdir -p uploads extensions database

Шаг 3. Написать docker-compose.yml

Создайте файл ~/directus/docker-compose.yml со следующим содержимым:

services:
	database:
		image: postgres:16-alpine
		restart: unless-stopped
		volumes:
			- ./database:/var/lib/postgresql/data
		environment:
			POSTGRES_USER: directus
			POSTGRES_PASSWORD: ${DB_PASSWORD}
			POSTGRES_DB: directus
		healthcheck:
			test: ["CMD", "pg_isready", "-U", "directus"]
			interval: 10s
			timeout: 5s
			retries: 5

	cache:
		image: redis:7-alpine
		restart: unless-stopped

	directus:
		image: directus/directus:latest
		restart: unless-stopped
		ports:
			- "127.0.0.1:8055:8055"  # слушаем только локально, наружу через Caddy
		volumes:
			- ./uploads:/directus/uploads
			- ./extensions:/directus/extensions
		depends_on:
			database:
				condition: service_healthy
		environment:
			KEY: ${DIRECTUS_KEY}
			SECRET: ${DIRECTUS_SECRET}

			DB_CLIENT: pg
			DB_HOST: database
			DB_PORT: 5432
			DB_DATABASE: directus
			DB_USER: directus
			DB_PASSWORD: ${DB_PASSWORD}

			CACHE_ENABLED: "true"
			CACHE_STORE: redis
			REDIS: redis://cache:6379

			ADMIN_EMAIL: ${ADMIN_EMAIL}
			ADMIN_PASSWORD: ${ADMIN_PASSWORD}

			PUBLIC_URL: https://concms.pimenov.ai

Шаг 4. Заполнить .env

Рядом с docker-compose.yml создаём .env. Секреты генерируются один раз и никуда не коммитятся:

openssl rand -hex 16  # для KEY
openssl rand -hex 32  # для SECRET и DB_PASSWORD

Пример .env:

DB_PASSWORD=<сгенерированное значение>
DIRECTUS_KEY=<uuid>
DIRECTUS_SECRET=<строка 32+ символов>
ADMIN_EMAIL=sergey@pimenov.am
ADMIN_PASSWORD=<надёжный пароль>
⚠️
Важно. .env не должен попадать в git — сразу добавьте его в .gitignore. На сервере ставим права строго для владельца: chmod 600 .env.

Шаг 5. Запустить и проверить

# Запускаем все сервисы в фоне
docker compose up -d

# Смотрим логи Directus
docker compose logs -f directus

При первом старте Directus создаст схему и админа. Когда в логах появится строка Server started at http://0.0.0.0:8055 — сервис готов.

Шаг 6. Поднять HTTPS через Caddy

Caddy выдаёт сертификаты Let's Encrypt автоматически — не нужно ни certbot, ни руками писать конфиги SSL.

sudo apt install -y caddy
sudo nano /etc/caddy/Caddyfile

Содержимое Caddyfile:

concms.pimenov.ai {
	reverse_proxy 127.0.0.1:8055
	encode gzip
}
sudo systemctl reload caddy

Через минуту https://concms.pimenov.ai работает с валидным сертификатом.

Шаг 7. Настроить бэкапы

Минимум — ежедневный дамп базы и архив загруженных файлов:

# ~/directus/backup.sh
#!/bin/bash
STAMP=$(date +%Y%m%d-%H%M)
mkdir -p ~/backups

# Дамп PostgreSQL
docker compose exec -T database pg_dump -U directus directus | gzip > ~/backups/db-$STAMP.sql.gz

# Архив медиа
tar -czf ~/backups/uploads-$STAMP.tar.gz -C ~/directus uploads

# Удаляем бэкапы старше 14 дней
find ~/backups -type f -mtime +14 -delete

Добавляем в cron:

crontab -e
# Каждый день в 03:15
15 3 * * * /bin/bash /home/$USER/directus/backup.sh >> /home/$USER/backups/backup.log 2>&1
💡
Бэкап, лежащий рядом с продом, — это не бэкап. Настройте выгрузку дампов на второй сервер, S3 или Backblaze B2, чтобы в момент сбоя вам было куда восстанавливаться.

Шаг 8. Обновлять

cd ~/directus
docker compose pull
docker compose up -d
docker compose logs -f directus  # проверяем, что миграции прошли

Directus сам накатывает миграции схемы при запуске. Перед любым мажорным апдейтом — обязательный свежий бэкап.


Типовые сценарии хаба

Мультисайтовая публикация

  1. В коллекции sites — записи pimenov.ai, reload.pimenov.ai, pimenov.ru
  2. В articles обязательное поле-связь site на эту коллекцию
  3. Политика Public отдаёт только status = published
  4. Каждый фронтенд на Astro тянет свой срез по filter[site][_eq]
  5. Кэш на уровне фронтенда (Astro ISR) и Redis на уровне Directus

База знаний для агентов

  1. Коллекция knowledge_docs — канонические документы
  2. Flow на обновление документа режет текст на чанки и считает эмбеддинги в knowledge_chunks через pgvector
  3. Агент-ресёрчер через MCP делает семантический поиск по чанкам и отдаёт релевантные куски в контекст LLM
  4. Обновление источника = автоматическое обновление индекса. Агент всегда работает с актуальной базой

Конвейер «агент → черновик → публикация»

  1. Внешний оркестратор (у меня это OpenClaw) получает задачу «написать статью про X»
  2. Агент через MCP читает базу знаний и предыдущие статьи по теме
  3. Пишет черновик в articles со статусом draft, автор — токен агента
  4. Flow отправляет уведомление в Telegram автору
  5. Автор правит в админке, меняет статус на published
  6. Webhook запускает ребилд сайта

Чего стоит избегать

  • ❌ Один токен на всех агентов — теряете аудит и не можете точечно отозвать доступ
  • ❌ Directus наружу без HTTPS и rate-limit — в продакшене обязателен reverse proxy с ограничениями
  • ❌ Хранить API-ключи в полях коллекций — для секретов есть .env и переменные окружения Flows
  • ❌ Смешивать оперативные данные и контент в одной схеме без политик
  • ❌ Разрешать агентам сразу публиковать — статус draft и человеческая валидация до published экономят много нервов
  • ❌ Игнорировать бэкапы — без крона и выноса дампов восстановление после сбоя невозможно
  • ❌ Использовать SQLite для контентного хаба в продакшене — для мультисайта и параллельной записи нужен PostgreSQL

Чеклист перед запуском в прод


Ссылки


По теме

Если вы уже чувствуете, что один сайт и один Notion перестают вмещать всё, что вы делаете, — такой контентный хаб обычно проще спроектировать один раз аккуратно, чем потом вытаскивать контент из пяти разных систем. Если хочется разобрать именно вашу конфигурацию — напишите, посмотрим вместе.

flowchart LR DB[(PostgreSQL)] --> D[Directus] D -->|REST / GraphQL| S1["pimenov.ai"] D -->|REST / GraphQL| S2["reload.pimenov.ai"] D -->|REST / GraphQL| S3["pimenov.ru"] D -->|MCP| A1["ИИ-агенты контента"] D -->|Flows| A2["Автоматизации"] D -->|App| U["Редакторы"]