Запуск 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 | Продакшен-режим, без автоперезагрузки, без конструктора |
Переменные окружения и секреты#
Новый проект получает файл .env с этими записями, каждая из которых заполнена случайным значением генератором проекта. В продакшене каждая из них должна быть задана, уникальна для этого окружения и неизменна со дня запуска.
| Переменная | Кто читает | Что ломается при её смене |
|---|---|---|
APP_KEYS | config/server.js | Подписанные cookie сессий становятся недействительными |
API_TOKEN_SALT | config/admin.js | Перестают работать все API-токены |
ADMIN_JWT_SECRET | config/admin.js | Все администраторы разлогиниваются |
TRANSFER_TOKEN_SALT | config/admin.js | Перестают работать токены передачи |
JWT_SECRET | config/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 он есть, обращайтесь с ним так же.
Остальная часть файла - обычные настройки времени выполнения:
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:
$ 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_CLIENT | sqlite, postgres или mysql | sqlite |
DATABASE_FILENAME | Путь к файлу SQLite | .tmp/data.db |
DATABASE_HOST | Адрес сервера | localhost |
DATABASE_PORT | Порт сервера | 5432 для PostgreSQL |
DATABASE_NAME | Имя базы | strapi |
DATABASE_USERNAME | Логин | strapi |
DATABASE_PASSWORD | Пароль | strapi |
DATABASE_SSL | TLS до базы | false |
DATABASE_SCHEMA | Схема PostgreSQL | public |
DATABASE_POOL_MIN / MAX | Пул соединений | 2 / 10 |
SQLite - это один файл, и база данных живёт внутри процесса. Для маркетингового сайта, портфолио или CMS для документации с одним-двумя редакторами и фронтендом, который собирается статически или кэшируется, её вполне достаточно: чтение быстрое, сетевого перехода нет, и больше ничего не нужно запускать или оплачивать. Её пределы - запись и копирование. Записи выполняются последовательно, так что загруженная админка плюс импорт по вебхуку выстроятся в очередь. А файл живой: копия data.db, снятая в момент записи Strapi, может не открыться.
Обратите внимание на путь по умолчанию. .tmp/data.db лежит в папке, чьё имя так и просит её удалить, поэтому на долгоживущем сервере либо оставьте её в покое осознанно, либо перенесите базу через DATABASE_FILENAME=data/strapi.db и убедитесь, что папка существует.
PostgreSQL - правильный выбор, как только появляется хоть что-то из этого: несколько редакторов, работающих одновременно, публичный API с реальным трафиком, контент, импортируемый скриптами, или потребность в согласованных бэкапах через pg_dump без остановки сайта. Для него в проекте нужен пакет pg:
$ npm install pgDATABASE_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 адресацию.
$ npm install @strapi/provider-upload-aws-s3module.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:
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 как минимум.
Три правила уберегут сборку от проблем:
- Собирайте с заданной
NODE_ENV=production, чтобы панель была минифицирована и собрана под продакшен-конфигурацию. - Пересобирайте после всего, что меняет админку: нового плагина, обновления Strapi, изменения
config/admin.js, смены пути админки или публичного URL. - Если сборка падает с ошибкой кучи JavaScript, поднимите лимит Node только для этой команды через
NODE_OPTIONS=--max-old-space-size=1536(меньше объёма памяти сервера) и прочитайте лимиты памяти Node, чтобы понять, почему значение по умолчанию - это не RAM вашего тарифа.
На сервере с жёстким лимитом памяти сборка, которая его превысила, не замедляется и не приходит в себя; процесс убивается. Симптом - сборка, которая обрывается на полпути без собственной ошибки. Этот случай лечится тарифом побольше на время сборки или флагом кучи выше, а не повторными попытками.
Публичный URL, путь админки и обратный прокси#
Strapi слушает HOST и PORT - по умолчанию 0.0.0.0:1337 - и ожидает перед собой обратный прокси для TLS и домена. Две настройки в 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, которые создают переносимый архив, не зависящий от движка базы:
$ 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 поставляется с инструментом обновления, который поднимает версии пакетов и выполняет преобразования кода:
$ npx @strapi/upgrade patch --dry$ npx @strapi/upgrade minor$ npx @strapi/upgrade majorpatch и minor остаются в пределах текущей мажорной версии; major - путь со Strapi 4 на 5, и это проект миграции, а не обновление: меняются плагины, собственный код и база данных. --dry показывает, что произойдёт, ничего не записывая.
Порядок, который работает:
- Сделайте бэкап базы данных и загрузок и храните архив не на этом сервере.
- Выполните обновление на копии для разработки со свежей копией продакшен-данных.
- Запустите
npm run build, стартуйте приложение и пройдитесь по админке и по API, которым пользуется ваш фронтенд. - Закоммитьте результат вместе с lock-файлом и задеплойте именно этот коммит.
- Пересоберите админку на сервере и запустите её; следите за логом первого запуска, потому что изменения схемы базы выполняются именно тогда.
Проверьте и версию 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-токены в архив не попадают. Держите также бэкап на уровне базы данных, а экспорт рассматривайте как инструмент миграции и переноса.


