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.