Directus на VPS в Docker: своя админка и API поверх готовой базы

Directus — это headless-платформа, которая не создаёт собственную базу данных, а надстраивается поверх уже существующей. Она читает структуру ваших таблиц и превращает их в REST и GraphQL API с готовой админкой, не перенося и не переименовывая ни одной строки данных. Если у вас есть рабочая база от старого сайта, CRM или самописного бэкенда, современный интерфейс редактирования и API появляются за час, а не за неделю миграции.

Directus не владеет вашими данными: он добавляет в базу только служебные таблицы с префиксом directus_ (роли, права, поля, пользователи панели), а ваши таблицы остаются нетронутыми. При этом таблица не станет видимой коллекцией лишь потому, что существует, — пока вы не подтвердите её в настройках. Это архитектурное решение, но новичков оно ловит регулярно.

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

  • VPS на Ubuntu 24.04 LTS. Минимум — 1 vCPU, 1 ГБ RAM, 20 ГБ SSD; комфортно — 2 vCPU, 2–4 ГБ RAM, 40 ГБ SSD. Подобрать сервер можно в нашем каталоге VPS, например у Timeweb Cloud.
  • Существующая база: PostgreSQL (рекомендуется), MySQL/MariaDB, SQLite, MS SQL или Oracle. Один инстанс подключается только к одной базе.
  • Отдельный SQL-пользователь с правами на нужную базу.
  • Актуальная LTS-ветка Node.js, если ставите без Docker.

Про память: если СУБД на том же сервере, RAM считайте отдельно под неё — 1 ГБ на связку под нагрузкой мало.

Установка пошагово

1. Сервер. Обновите систему и создайте отдельного пользователя:

apt update && apt upgrade -y
adduser directus
usermod -aG sudo directus
su - directus

Firewall — порт 8055 наружу не открываем:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

2. Node.js — только свежая LTS через nvm, версия из репозитория Ubuntu устарела:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts && nvm use --lts
sudo apt install -y build-essential git

3. Пользователь базы. Ключевая строка — грант на схему public: с PostgreSQL 15 она не даёт права CREATE никому, кроме владельца базы, и без этого bootstrap упадёт.

CREATE USER directus_user WITH ENCRYPTED PASSWORD 'сложный_пароль';
GRANT ALL PRIVILEGES ON DATABASE legacy_shop TO directus_user;
\c legacy_shop
GRANT ALL ON SCHEMA public TO directus_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO directus_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO directus_user;

Сразу сделайте дамп — страховка перед первым bootstrap на боевой базе: pg_dump -U postgres legacy_shop > ~/legacy_shop_backup_$(date +%F).sql.

4. Установка Directus вручную — мастер create directus-project создаёт новую базу с нуля, а нам нужна привязка к существующей:

mkdir -p /opt/directus
sudo chown directus:directus /opt/directus
cd /opt/directus
npm init -y
npm install directus

Секреты и .env:

KEY=$(openssl rand -hex 16)
SECRET=$(openssl rand -hex 32)
KEY=сгенерированный_ключ
SECRET=сгенерированный_секрет
DB_CLIENT=pg
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=legacy_shop
DB_USER=directus_user
DB_PASSWORD=сложный_пароль
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=другой_сложный_пароль
PUBLIC_URL=https://directus.example.com
PORT=8055
HOST=127.0.0.1

Значения DB_CLIENT зависят от СУБД: pg, mysql, sqlite3, mssql, oracledb.

5. Bootstrap и запуск. Именно эта команда создаёт таблицы directus_* в вашей базе, не затрагивая остальные:

npx directus bootstrap
npx directus start

Появилась строка о запуске на порту 8055 — остановите процесс (Ctrl+C) и настройте автозапуск через systemd. Создайте /etc/systemd/system/directus.service (WorkingDirectory=/opt/directus, ExecStart с точным путём к node из which node, Restart=on-failure) и примените:

sudo systemctl daemon-reload
sudo systemctl enable --now directus

Логи — journalctl -u directus -f; если сервис не стартует, чаще всего виноват путь к node или права на .env.

6. Админка. Откройте http://127.0.0.1:8055 через SSH-туннель или по домену и в Settings → Data Model отметьте нужные таблицы управляемыми — только тогда они появятся в меню и API. ADMIN_PASSWORD действует лишь при первом запуске.

Docker Compose (готовый конфиг)

Когда база существует отдельно, удобнее контейнер. Ниже минимальный стек — Directus, Redis для кеша и WebSocket-синхронизации, том для файлов; базу не добавляем, она подключается как внешняя.

services:
  directus:
    image: directus/directus:11
    container_name: directus
    restart: unless-stopped
    ports:
      - "127.0.0.1:8055:8055"
    volumes:
      - directus_uploads:/directus/uploads
      - directus_extensions:/directus/extensions
      - ./templates:/directus/templates
    environment:
      KEY: ${DIRECTUS_KEY}
      SECRET: ${DIRECTUS_SECRET}
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      DB_CLIENT: pg
      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      DB_DATABASE: ${DB_DATABASE}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_SSL__REJECT_UNAUTHORIZED: "false"
      PUBLIC_URL: https://cms.example.com
      CORS_ENABLED: "true"
      CORS_ORIGIN: "https://example.com"
      WEBSOCKETS_ENABLED: "true"
      CACHE_ENABLED: "true"
      CACHE_STORE: redis
      REDIS: redis://redis:6379
      RATE_LIMITER_ENABLED: "true"
      STORAGE_LOCATIONS: local
      STORAGE_LOCAL_ROOT: /directus/uploads
    depends_on:
      - redis
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
volumes:
  directus_uploads:
  directus_extensions:
  redis_data:

Рядом положите .env:

DIRECTUS_KEY=первый_рандомный_хеш
DIRECTUS_SECRET=второй_рандомный_хеш
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=пароль_минимум_16_символов
DB_HOST=10.0.0.5
DB_PORT=5432
DB_DATABASE=my_existing_db
DB_USER=directus_ro
DB_PASSWORD=пароль_от_бд

Два хеша получите командой openssl rand -hex 32, для MySQL поставьте DB_CLIENT: mysql и порт 3306. Базу на другом сервере подключайте через WireGuard или SSH-туннель, а не открытым портом наружу. Порт 8055 проброшен только на localhost, поэтому наружу отдаём через reverse-proxy:

docker compose up -d
server {
    listen 443 ssl http2;
    server_name cms.example.com;
    client_max_body_size 50m;
    location / {
        proxy_pass http://127.0.0.1:8055;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Без заголовков Upgrade/Connection realtime-функции админки молча не заработают. А client_max_body_size обязателен: по умолчанию nginx режет запрос на 1 МБ, и загрузка файлов падает с 413.

Частые ошибки и решения

ECONNREFUSED 127.0.0.1:5432 или KnexTimeoutError. Проверьте переменные DB_*. Если Postgres и Directus в разных контейнерах одной сети, DB_HOST — имя сервиса (например, db), а не 127.0.0.1. Под нагрузкой увеличьте пул и таймаут (DB_POOL__MIN=2, DB_POOL__MAX=10, DB_CONNECTION_TIMEOUT=30000), но на VPS с 2–4 ГБ RAM не ставьте MAX выше 10–15 — упрётесь в лимит соединений PostgreSQL.

permission denied for schema public. PostgreSQL 15+ не даёт прав CREATE по умолчанию — лечится грантом из шага 3.

Коллекции не отображаются. Проверьте SELECT collection, icon, singleton FROM directus_collections; — если вашей таблицы там нет, Directus о ней не знает. Переносите схему снапшотами (npx directus schema snapshot ./snapshot.yaml, затем schema diff и schema apply): новая коллекция, сделанная на staging через UI, не появится на проде без явного apply.

Белый экран или 502 за nginx. Почти всегда виноват PUBLIC_URL: со значением по умолчанию (http://localhost:8055) админка тянет статику по localhost из браузера пользователя — отсюда 404 по JS/CSS.

403 вместо данных. Роль Public изначально не имеет доступа ни к одной коллекции. Различайте права на уровне коллекции и на уровне отдельных полей: API вернёт объект без нужного поля, а не ошибку. Для отладки возьмите статический токен и проверьте запрос напрямую:

curl -H "Authorization: Bearer ТОКЕН" https://cms.example.com/items/products?limit=5

Тоже 403 — проблема в правах роли, а не во фронтенде.

CORS при отдельном фронтенде. Для SPA с другого домена задайте CORS_ENABLED=true, CORS_ORIGIN и CORS_CREDENTIALS=true: по умолчанию CORS_ORIGIN=false молча блокирует кросс-доменные запросы.

Контейнер перезапускается по кругу. Гонка старта: Directus поднимается раньше Postgres. Добавьте depends_on с condition: service_healthy и healthcheck для базы.

Бэкап и обновление

Бэкап Directus — это не одна команда, а три независимых слоя: база, загруженные файлы и .env. Сохраните только один — получите либо пустую админку, либо битые ссылки на картинки.

Отдельно про KEY и SECRET: SECRET подписывает JWT-сессии, а часть настроек лежит в базе зашифрованной именно этими значениями. Восстановление с другими KEY/SECRET сделает такие поля нечитаемыми, а все сессии — невалидными. Поэтому .env бэкапится вместе с базой, одним архивом.

Дамп базы (формат custom даёт сжатие и выборочное восстановление):

docker exec -t directus-db pg_dump -U directus -d directus --format=custom \
  > /backups/directus/db_$(date +%F_%H%M).dump

Сразу проверьте его целостность: pg_restore --list db.dump | head -20. Для MySQL — mysqldump с флагами --single-transaction --routines, иначе дамп на активной базе может получиться несогласованным.

Файлы из тома:

docker run --rm -v directus_uploads:/data -v /backups/directus:/backup \
  alpine tar czf /backup/uploads_$(date +%F).tar.gz -C /data .

Если файлы пишутся прямо в S3/MinIO, сервер в этой части бэкапить не нужно — копируйте бакет средствами хранилища.

Snapshot схемы — независимый слой: только структура, без данных, для переноса конфигурации между окружениями:

docker exec directus npx directus schema snapshot ./snapshot.yaml
docker cp directus:/directus/snapshot.yaml /backups/directus/schema_$(date +%F).yaml

Соберите всё в один скрипт с датированной папкой и ротацией (например, 14 последних снимков) и поставьте в cron на ночь: 0 3 * * * /opt/scripts/directus-backup.sh. Копия на том же сервере не спасает от отказа диска — папку бэкапов синхронизируйте наружу (другой сервер, S3, restic).

Восстановление — строго по порядку: поднять ту же версию образа, что была в бэкапе (миграции привязаны к версии), восстановить .env до старта контейнеров, развернуть пустую базу и накатить дамп с --clean --if-exists, вернуть файлы в том, запустить Directus и проверить логи на ошибки миграций. Грузящиеся превью картинок в админке подтвердят, что база и файлы синхронизированы корректно.

Обновление — смена тега и рестарт:

docker compose pull directus
docker compose up -d directus
docker compose logs -f directus

Перед мажорным обновлением читайте changelog: изредка нужна явная directus database migrate:latest, обычно миграции идут автоматически при старте.

Итог

Directus даёт то, чего не может классическая headless-CMS: API и админку поверх базы, которую нельзя или не хочется трогать. Ключевые точки — корректные гранты SQL-пользователю, правильный PUBLIC_URL за прокси и бэкап из трёх слоёв вместо одного дампа базы.

Дальше стоит развести доступ по ролям (Administrator — для настроек, для фронтенда — read-only роль со статическим токеном), поставить бэкапный скрипт в cron с копированием наружу и при росте трафика перевести медиатеку на S3. Если сервера ещё нет — присмотрите конфигурацию в нашем каталоге VPS: для Directus и PostgreSQL комфортно начинать с 2 vCPU и 4 ГБ RAM.