RE:NODE

Strapi в продакшене: переменные окружения, база, загрузки и обновления

Автор Команда RE:NODE
15 мин чтения


Запуск Strapi в продакшене сводится к шести решениям, которые принимаются один раз и записываются: набор секретов в .env, которые после запуска никогда не меняются; база данных, которую можно забэкапить (SQLite для одного небольшого сайта, PostgreSQL для всего, где есть редакторы и трафик); место для загрузок, которое переживёт пересборку; админ-панель, собранная с NODE_ENV=production; публичный url, совпадающий с тем, что отдаёт обратный прокси; и порядок обновления, который начинается с бэкапа. Чтобы спокойно собрать админ-панель, Strapi нужно около 2 ГБ памяти, а для обслуживания запросов после этого - гораздо меньше. Сделайте эти шесть вещей правильно, и Strapi будет тихим процессом Node на порту 1337. Ошибитесь в одной - и узнаете об этом в тот день, когда редактор войдёт в систему и все API-токены перестанут работать.

Это руководство описывает Strapi 5, текущую мажорную версию, и отмечает, где отличается Strapi 4. Имена файлов показаны как .js; в проекте на TypeScript те же файлы называются .ts.

Как устроен сервер приложения Strapi#

Strapi - это приложение на Node.js из двух половин, которые работают в одном процессе. Первая - API контента: REST (и GraphQL, если добавить плагин) под /api, сгенерированный из определённых вами типов контента. Вторая - админ-панель, React-приложение по адресу /admin, в котором редакторы пишут контент, а вы в режиме разработки создаёте типы контента.

Это разделение объясняет самое неожиданное правило продакшен-Strapi: Content-Type Builder работает только в режиме разработки. strapi develop следит за проектом и перезапускается при изменении модели; strapi start этого не делает, и конструктор доступен только для чтения. Типы контента - это код, JSON-схемы в src/api и src/components, и им место в Git. Вы меняете их на копии для разработки, коммитите и деплоите - точно так же, как миграцию базы данных в любом другом фреймворке.

При запуске Strapi сравнивает эти схемы с базой данных и приводит таблицы в соответствие. Это удобно, но к этому нужно относиться с уважением: переименованное поле для базы данных выглядит как одно удалённое поле и одно новое, и данные из старого столбца не переезжают под новое имя. Переименовывайте поля сначала на копии продакшен-данных, никогда на живом сайте.

Три команды, которые вам понадобятся; все они уже определены в package.json, с которым создаётся новый проект:

КомандаЧто делает
npm run developРежим разработки, автоперезагрузка, Content-Type Builder включён
npm run buildСобирает админ-панель (и компилирует TypeScript)
npm run startПродакшен-режим, без автоперезагрузки, без конструктора
HTTPS 443HTTP 1337провайдер загрузокссылки на медиаБраузерсайт или редакторОбратный проксиHTTPS, ваш доменStrapiNode, порт 1337База данныхSQLite или PostgreSQLБакет S3загруженные медиа
Путь запроса от браузера до Strapi и его данных

Переменные окружения и секреты#

Новый проект получает файл .env с этими записями, каждая из которых заполнена случайным значением генератором проекта. В продакшене каждая из них должна быть задана, уникальна для этого окружения и неизменна со дня запуска.

ПеременнаяКто читаетЧто ломается при её смене
APP_KEYSconfig/server.jsПодписанные cookie сессий становятся недействительными
API_TOKEN_SALTconfig/admin.jsПерестают работать все API-токены
ADMIN_JWT_SECRETconfig/admin.jsВсе администраторы разлогиниваются
TRANSFER_TOKEN_SALTconfig/admin.jsПерестают работать токены передачи
JWT_SECRETconfig/plugins.jsВходы пользователей (Users and Permissions) истекают

APP_KEYS - это список через запятую, а не одно значение. Strapi читает его через env.array('APP_KEYS') и использует ключи для подписи cookie; несколько ключей позволяют их ротировать. API_TOKEN_SALT используется для хеширования API-токенов перед сохранением, поэтому его смена молча делает недействительным каждый токен, который хранят ваш фронтенд, сборочный конвейер или мобильное приложение. JWT_SECRET принадлежит плагину Users and Permissions - тому, что выдаёт токены людям, зарегистрировавшимся на вашем сайте, в отличие от редакторов в админке. Недавние проекты на Strapi 5 также генерируют ENCRYPTION_KEY для зашифрованных секретов админки; если в вашем .env он есть, обращайтесь с ним так же.

Остальная часть файла - обычные настройки времени выполнения:

.env
HOST=0.0.0.0PORT=1337NODE_ENV=productionAPP_KEYS=key1base64==,key2base64==,key3base64==,key4base64==API_TOKEN_SALT=replace-with-random-base64ADMIN_JWT_SECRET=replace-with-random-base64TRANSFER_TOKEN_SALT=replace-with-random-base64JWT_SECRET=replace-with-random-base64STRAPI_TELEMETRY_DISABLED=true

Чтобы сгенерировать значение, подойдёт любой источник из 16 и более случайных байт в кодировке base64:

bash
$ openssl rand -base64 32$ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Если APP_KEYS отсутствует, Strapi отказывается стартовать с ошибкой от middleware strapi::session, которая сообщает, что ключи приложения обязательны. Это хороший отказ: громкий, при загрузке и очевидный. Плохие отказы - тихие, описанные выше: они случаются, когда кто-то перегенерирует секреты на работающем сайте. Статья переменные окружения и секреты описывает общую дисциплину: как держать их вне кода и вне логов.

SQLite или PostgreSQL#

Strapi поддерживает SQLite, PostgreSQL и MySQL; выбор делается переменной DATABASE_CLIENT, а настройка - в config/database.js. Сгенерированный файл читает все параметры подключения из окружения, так что переход сводится к переменным и установке нужного драйвера.

ПеременнаяДля чегоЗначение по умолчанию в сгенерированном конфиге
DATABASE_CLIENTsqlite, postgres или mysqlsqlite
DATABASE_FILENAMEПуть к файлу SQLite.tmp/data.db
DATABASE_HOSTАдрес сервераlocalhost
DATABASE_PORTПорт сервера5432 для PostgreSQL
DATABASE_NAMEИмя базыstrapi
DATABASE_USERNAMEЛогинstrapi
DATABASE_PASSWORDПарольstrapi
DATABASE_SSLTLS до базыfalse
DATABASE_SCHEMAСхема PostgreSQLpublic
DATABASE_POOL_MIN / MAXПул соединений2 / 10

SQLite - это один файл, и база данных живёт внутри процесса. Для маркетингового сайта, портфолио или CMS для документации с одним-двумя редакторами и фронтендом, который собирается статически или кэшируется, её вполне достаточно: чтение быстрое, сетевого перехода нет, и больше ничего не нужно запускать или оплачивать. Её пределы - запись и копирование. Записи выполняются последовательно, так что загруженная админка плюс импорт по вебхуку выстроятся в очередь. А файл живой: копия data.db, снятая в момент записи Strapi, может не открыться.

Обратите внимание на путь по умолчанию. .tmp/data.db лежит в папке, чьё имя так и просит её удалить, поэтому на долгоживущем сервере либо оставьте её в покое осознанно, либо перенесите базу через DATABASE_FILENAME=data/strapi.db и убедитесь, что папка существует.

PostgreSQL - правильный выбор, как только появляется хоть что-то из этого: несколько редакторов, работающих одновременно, публичный API с реальным трафиком, контент, импортируемый скриптами, или потребность в согласованных бэкапах через pg_dump без остановки сайта. Для него в проекте нужен пакет pg:

bash
$ npm install pg
.env
DATABASE_CLIENT=postgresDATABASE_HOST=db.example.netDATABASE_PORT=5432DATABASE_NAME=strapiDATABASE_USERNAME=strapiDATABASE_PASSWORD=long-random-passwordDATABASE_SSL=false

Если вы предпочитаете одну строку подключения, читается и DATABASE_URL. Ставьте DATABASE_SSL=true, когда база доступна через интернет, а не по частной сети; статья PostgreSQL sslmode объясняет, что на самом деле проверяет каждый уровень. Пула по умолчанию в 10 соединений хватает одному процессу Strapi; если процессов несколько, сложите их пулы и сравните с лимитом соединений базы, как объясняет статья пулы соединений и лимиты.

Перенос существующего сайта с SQLite на PostgreSQL делается собственными экспортом и импортом Strapi, а не конвертацией файла: экспортируйте из экземпляра на SQLite, направьте переменные на пустую базу PostgreSQL, запустите Strapi один раз, чтобы он создал таблицы, остановите его и выполните импорт. Команды приведены ниже, в разделе о бэкапах.

Загрузки: локальный диск или бакет S3#

По умолчанию плагин Upload хранит файлы в public/uploads, и Strapi отдаёт их сам. На одном сервере с постоянным диском это работает, и именно так стартует Strapi, установленный в один клик. К объектному хранилищу подталкивают три вещи: диск, заполняющийся изображениями и сгенерированными миниатюрами, процесс деплоя, который заменяет папку проекта, и желание отдавать медиа так, чтобы каждый запрос не проходил через Node.

Официальный S3-провайдер Strapi - @strapi/provider-upload-aws-s3. Несмотря на название, он работает с любым S3-совместимым сервисом, если указать ему endpoint и, для большинства сервисов не от AWS, path-style адресацию.

bash
$ npm install @strapi/provider-upload-aws-s3
config/plugins.js
module.exports = ({ env }) => ({  upload: {    config: {      provider: 'aws-s3',      providerOptions: {        baseUrl: env('S3_PUBLIC_URL'),        s3Options: {          credentials: {            accessKeyId: env('S3_ACCESS_KEY_ID'),            secretAccessKey: env('S3_ACCESS_SECRET'),          },          region: env('S3_REGION'),          endpoint: env('S3_ENDPOINT'),          forcePathStyle: true,          params: {            Bucket: env('S3_BUCKET'),          },        },      },      actionOptions: {        upload: {},        uploadStream: {},        delete: {},      },    },  },});

Имена переменных после env( вы выбираете сами; фиксирована только структура. baseUrl - публичный адрес, с которого отдаются файлы; он может отличаться от endpoint API, если перед бакетом стоит CDN или прокси. forcePathStyle: true заставляет отправлять запросы на endpoint/bucket/key вместо bucket.endpoint/key - статья path-style и virtual-hosted в S3 объясняет, почему это нужно большинству самостоятельно размещённых хранилищ.

Шаг, который пропускают все, - Content Security Policy админ-панели. Middleware strapi::security разрешает изображения только со своего источника, поэтому после смены провайдера загрузка проходит, но миниатюры в Media Library сломаны. Добавьте хост вашего бакета в img-src и media-src в config/middlewares.js:

config/middlewares.js
module.exports = [  'strapi::logger',  'strapi::errors',  {    name: 'strapi::security',    config: {      contentSecurityPolicy: {        useDefaults: true,        directives: {          'connect-src': ["'self'", 'https:'],          'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'files.example.com'],          'media-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'files.example.com'],          upgradeInsecureRequests: null,        },      },    },  },  'strapi::cors',  'strapi::poweredBy',  'strapi::query',  'strapi::body',  'strapi::session',  'strapi::favicon',  'strapi::public',];

Смена провайдера не переносит существующие файлы. Медиа, загруженные до изменения, по-прежнему указывают на /uploads/... на локальном диске; либо оставьте эти файлы на месте, либо планируйте переезд как экспорт, смену провайдера и импорт.

Сборка админки и память#

npm run build компилирует админ-панель в статические файлы. Это выполняется один раз на деплой, а не на каждый запрос, но это самое прожорливое по памяти действие Strapi: сборке React-приложения с админским кодом всех плагинов ненадолго может понадобиться заметно больше гигабайта, тогда как работающий после этого сервер часто держится на нескольких сотнях мегабайт. Документация Strapi по деплою указывает 2 ГБ RAM как минимум.

Три правила уберегут сборку от проблем:

  1. Собирайте с заданной NODE_ENV=production, чтобы панель была минифицирована и собрана под продакшен-конфигурацию.
  2. Пересобирайте после всего, что меняет админку: нового плагина, обновления Strapi, изменения config/admin.js, смены пути админки или публичного URL.
  3. Если сборка падает с ошибкой кучи JavaScript, поднимите лимит Node только для этой команды через NODE_OPTIONS=--max-old-space-size=1536 (меньше объёма памяти сервера) и прочитайте лимиты памяти Node, чтобы понять, почему значение по умолчанию - это не RAM вашего тарифа.

На сервере с жёстким лимитом памяти сборка, которая его превысила, не замедляется и не приходит в себя; процесс убивается. Симптом - сборка, которая обрывается на полпути без собственной ошибки. Этот случай лечится тарифом побольше на время сборки или флагом кучи выше, а не повторными попытками.

Публичный URL, путь админки и обратный прокси#

Strapi слушает HOST и PORT - по умолчанию 0.0.0.0:1337 - и ожидает перед собой обратный прокси для TLS и домена. Две настройки в config/server.js заставляют его вести себя там правильно:

config/server.js
module.exports = ({ env }) => ({  host: env('HOST', '0.0.0.0'),  port: env.int('PORT', 1337),  url: env('PUBLIC_URL', 'https://cms.example.com'),  app: {    keys: env.array('APP_KEYS'),  },});

url - публичный адрес сайта вместе со схемой. Strapi использует его для генерации абсолютных ссылок - собственного адреса админки, URL файлов от локального провайдера загрузок, ссылок в письмах для сброса пароля, - поэтому, если его не задать, получатся ссылки http://0.0.0.0:1337/..., которые работают только на самом сервере. Если вы отдаёте Strapi по подпути вроде https://example.com/cms, укажите путь в url и пересоберите админку.

Вторая настройка - доверие прокси. За прокси Strapi видит каждый запрос как обычный HTTP с адреса прокси, пока ему не скажут доверять заголовкам X-Forwarded-For и X-Forwarded-Proto. Опция называется proxy в config/server.js; её форма изменилась между Strapi 4 (логическое значение) и Strapi 5 (объект с отдельными ключами), поэтому сверьтесь со страницей настройки сервера для своей версии, а не копируйте старый фрагмент. Без неё классическая ошибка - Cannot send secure cookie over unencrypted connection, когда процесс входа пытается установить защищённую cookie.

Админ-панель по умолчанию находится по пути /admin, а путь задаётся в config/admin.js. Перенос не делает её защищённой - API всё равно отвечает, где бы она ни была, - но шума от сканеров становится меньше. Статья что делает обратный прокси подробно разбирает заголовки, а если перед Strapi стоит ваш собственный nginx, в руководстве по обратному прокси на nginx есть рабочий блок. Помните, что client_max_body_size в nginx по умолчанию равен 1m, и это блокирует любую загрузку больше одного мегабайта ответом 413 ещё до того, как запрос увидит Strapi.

Strapi предоставляет /_health, который отвечает пустым 204, когда сервер работает, - удобно для монитора доступности, которому не нужно входить в систему или дёргать API, зависящий от базы данных.

Бэкапы и восстановление#

Слоёв два, и нужны оба.

Первый - база данных плюс загрузки, скопированные как файлы. С PostgreSQL pg_dump снимает согласованный дамп, пока Strapi продолжает работать, - статья pg_dump и pg_restore разбирает форматы и восстановление. С SQLite безопасная копия файла снимается при остановленном Strapi или инструментом, использующим backup API самой SQLite; простое копирование файла посреди записи - лотерея. Загрузки в public/uploads - обычные файлы. Вместе с кодом проекта и вашим .env эти три части дают полное восстановление.

Второй слой - собственные команды передачи данных Strapi, которые создают переносимый архив, не зависящий от движка базы:

bash
$ npm run strapi export -- --no-encrypt -f strapi-backup$ npm run strapi import -- -f strapi-backup.tar.gz

Без --no-encrypt архив шифруется ключом, который у вас запросят, и получает суффикс .tar.gz.enc. В архиве лежат контент, загруженные файлы и конфигурация проекта. В нём нет администраторов и API-токенов, поэтому он не заменяет бэкап базы данных - это инструмент для переезда между SQLite и PostgreSQL или между серверами. Импорт перед записью удаляет существующий контент в целевом экземпляре, так что никогда не направляйте его не туда. strapi transfer делает то же самое вживую между двумя работающими экземплярами Strapi с помощью токена передачи - именно его защищает TRANSFER_TOKEN_SALT.

Что бы вы ни выбрали, хотя бы раз восстановите бэкап где-нибудь. Статья бэкапы, которые действительно восстанавливаются объясняет, почему бэкап, который никто не восстанавливал, - всего лишь гипотеза.

Безопасное обновление Strapi#

Strapi выпускает релизы часто, а админка пересобирается при каждом обновлении, поэтому обновления заслуживают отработанного порядка, а не npm update. Strapi 5 поставляется с инструментом обновления, который поднимает версии пакетов и выполняет преобразования кода:

bash
$ npx @strapi/upgrade patch --dry$ npx @strapi/upgrade minor$ npx @strapi/upgrade major

patch и minor остаются в пределах текущей мажорной версии; major - путь со Strapi 4 на 5, и это проект миграции, а не обновление: меняются плагины, собственный код и база данных. --dry показывает, что произойдёт, ничего не записывая.

Порядок, который работает:

  1. Сделайте бэкап базы данных и загрузок и храните архив не на этом сервере.
  2. Выполните обновление на копии для разработки со свежей копией продакшен-данных.
  3. Запустите npm run build, стартуйте приложение и пройдитесь по админке и по API, которым пользуется ваш фронтенд.
  4. Закоммитьте результат вместе с lock-файлом и задеплойте именно этот коммит.
  5. Пересоберите админку на сервере и запустите её; следите за логом первого запуска, потому что изменения схемы базы выполняются именно тогда.

Проверьте и версию Node.js. Каждый релиз Strapi поддерживает определённый диапазон чётных LTS-версий Node; работа на нечётной версии или на версии с истёкшей поддержкой - частая причина ошибок установки после обновления.

Strapi на RE:NODE#

На RE:NODE Strapi - приложение в один клик на странице хостинга приложений. Он устанавливается и собирается при первом запуске сервера, данные хранятся в SQLite на сервере, поэтому первым делом откройте /admin и создайте учётную запись администратора. Тарифы начинаются с 2 ГБ памяти, что соответствует потребностям сборки админки. Файл .env лежит в папке проекта, и его можно редактировать в файловом менеджере или по SFTP; прежде чем раздавать адрес, проверьте, что каждый секрет из таблицы выше содержит длинное случайное значение.

Панель закрывает эксплуатационные задачи, описанные в этом руководстве. Направьте A-запись на адрес, показанный у прокси-слота, и сертификат будет выпущен и продлён автоматически, а реальный IP клиента придёт в X-Forwarded-For. Слоты для бэкапов есть на каждом тарифе, бэкапы запускаются вручную или по вкладке Schedules и хранятся вне машины - см. бэкапы в руководствах. Поскольку SQLite - живой файл, планируйте бэкапы на тихий час или сначала останавливайте сервер. Если вы переросли SQLite, сервер PostgreSQL даст вам значения DATABASE_*, которые останется вставить, а тариф объектного хранилища S3 - endpoint, ключи и первый бакет для провайдера загрузок, с path-style адресацией и любым именем региона.

При достижении лимита памяти сервер останавливается и перезапускается начисто, а не уходит в своп, - это и есть симптом убитой сборки, описанный выше: если npm run build молча обрывается, причина в этом.

Устранение неполадок#

Вход в админку работает, а после деплоя всех разлогинило. Изменился ADMIN_JWT_SECRET, обычно потому, что сгенерировали новый .env. Верните исходное значение.

После переезда каждый API-токен возвращает `401`. API_TOKEN_SALT отличается от того, с которым токены создавались. Верните его или создайте новые токены и обновите всех клиентов.

Миниатюры в Media Library сломаны, хотя файлы загружаются. Хоста бакета нет в img-src в config/middlewares.js.

Ссылки в письмах и URL медиа указывают на `0.0.0.0:1337`. В config/server.js не задан url. Задайте его и пересоберите админку.

После обновления админка показывает старую версию. Админку не пересобрали. Выполните npm run build и перезапустите.

Загрузки больше 1 МБ падают с `413`. Это лимит размера тела запроса на обратном прокси, а не Strapi.

Типы контента нельзя редактировать в админке. Вы в продакшен-режиме, и так и должно быть. Меняйте их в режиме разработки и деплойте.

FAQ#

Достаточно ли SQLite для Strapi в продакшене?

Для небольшого сайта с одним-двумя редакторами и кэшируемым или статически собранным фронтендом - да. Она плохо справляется со множеством одновременных записей и требует аккуратности при бэкапе, поэтому переходите на PostgreSQL, как только несколько человек начинают редактировать одновременно или API получает реальный трафик.

Можно ли потом поменять APP_KEYS или другие секреты?

Можно, но у каждой смены есть цена: администраторы разлогиниваются, API-токены умирают или пользовательские сессии заканчиваются - в зависимости от того, какой секрет вы сменили. Ротируйте осознанно, по одному, и заранее предупредите тех, у кого есть токены.

Нужна ли пересборка после изменения .env?

Не для учётных данных базы и не для секретов: они читаются при старте сервера, достаточно перезапуска. Пересборка нужна после смены публичного URL, пути админки, плагинов или версии Strapi.

Почему на продакшен-сервере нельзя создавать типы контента?

Strapi намеренно отключает Content-Type Builder вне режима разработки. Модели - это код, и их изменение на живом сервере поменяло бы схему базы данных без всякой записи в системе контроля версий.

Сохраняет ли strapi export моих администраторов?

Нет. Администраторы и API-токены в архив не попадают. Держите также бэкап на уровне базы данных, а экспорт рассматривайте как инструмент миграции и переноса.