Перейти к основному содержимому

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

Полный список с рабочими локальными значениями — .env.example в корне. Здесь разбор по группам: что обязательно, что обязательно сменить перед публикацией наружу.

Два файла, а не один​

ФайлКто читает
.env в корнеDocker Compose и все сервисы стека
apps/backend/.envMedusa CLI с хоста (db:migrate, medusa user)

Значения в них должны совпадать — как минимум DATABASE_URL и MEDUSA_FF_RBAC. Расхождение даёт падения, которые выглядят как что угодно, только не как ошибка конфигурации.

Секреты: сменить обязательно​

Значения из .env.example рабочие только локально. Перед выкладкой наружу генерируйте независимые случайные значения (openssl rand -base64 32) для каждого окружения:

JWT_SECRET, COOKIE_SECRET, AUTH_MFA_ENCRYPTION_KEY, MOYSKLAD_ENCRYPTION_KEY, PAYMENT_PROVIDER_CONFIG_ENCRYPTION_KEY, SHIPPING_PROVIDER_CONFIG_ENCRYPTION_KEY, POSTGRES_PASSWORD, MINIO_ROOT_PASSWORD, S3-ключи, все STRAPI_* секреты, MEILI_MASTER_KEY, ADMIN_PASSWORD.

COOKIE_SECRET подписывает cookie-сессии Medusa — независимо от JWT_SECRET, который отвечает за токены аутентификации (medusa-config.ts:30). Ротация одного не затрагивает другой; смена COOKIE_SECRET разлогинивает всех, у кого была активная сессионная cookie, а смена JWT_SECRET инвалидирует выданные JWT.

Ключи шифрования нельзя менять после запуска

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

Адреса и CORS​

MEDUSA_BACKEND_URL, STOREFRONT_URL, STOREFRONT_PUBLIC_URL, STORE_CORS, ADMIN_CORS, AUTH_CORS.

STORE_CORS сверяет строку точно: http://localhost:8000 и http://127.0.0.1:8000 — разные источники. ADMIN_CORS и AUTH_CORS управляют доступом независимо друг от друга — первый разрешает источники для Admin API, второй отдельно для auth-роутов (/auth/*); задать один без другого — обычная причина, по которой браузер пропускает запросы к админке, но блокирует логин (или наоборот).

VITE_BACKEND_URL — отдельный build/runtime-параметр именно кастомных Admin API-расширений (admin/lib/client.ts:4, sdk.ts:4), не связан с MEDUSA_INTERNAL_URL витрины. Не задан — используется текущий origin страницы (относительный путь /).

MEDUSA_INTERNAL_URL — отдельная переменная, не MEDUSA_BACKEND_URL

Витрина проксирует /store, /auth и /catalog/* на backend через middleware, а не через rewrites() (адрес backend известен только в рантайме, а rewrites() вычисляется на этапе сборки). Без MEDUSA_INTERNAL_URL эти пути остаются непроксированными вместо обращения к backend (middleware.ts:26), а серверные API-вызовы отдельно требуют абсолютный HTTP(S)-адрес в этой же переменной — иное значение или отсутствие переменной там даёт явную ошибку (lib/api/base-url.ts:11). Next.js-витрина MEDUSA_BACKEND_URL не использует вовсе — эту переменную читают только сторонние по отношению к витрине инструменты: бенчмарк поиска (apps/backend/src/scripts/benchmark-catalog-search.ts) и e2e-обвязка (apps/e2e/playwright.config.ts, apps/e2e/scripts/visual-check.ts) для выбора адреса проверяемого backend; спутать её с MEDUSA_INTERNAL_URL даёт «backend вроде настроен, а Store API не отвечает».

Флаги поведения​

ПеременнаяСмысл
MEDUSA_FF_RBACРоли и права в админке. Должна быть true в обоих env-файлах
SESSION_COOKIE_SECUREfalse только пока перед стеком нет TLS. Появился HTTPS-прокси — ставьте true
MEDUSA_ADMIN_ONBOARDING_TYPEskip — не показывать мастер первого запуска
YOOKASSA_WEBHOOK_TRUSTED_PROXY_CIDRSCIDR прокси, которым разрешено подставлять X-Forwarded-For в вебхуках ЮKassa. Пусто, если backend смотрит наружу напрямую
SESSION_COOKIE_SECURE=false — осознанный компромисс

Medusa форсирует Secure для сессионной cookie покупателя при NODE_ENV=production. Без TLS браузер такую cookie не сохранит, и вход в личный кабинет молча сломается. Compose сам TLS не терминирует, поэтому по умолчанию false. Как только появился nginx/Caddy/облачный балансировщик с HTTPS — переключите на true.

NODE_ENV=test отключает часть модулей​

При NODE_ENV=test отключаются ApiShip-плагин, S3, Redis cache/event/workflow engine, Meilisearch и fulfillment-провайдеры (medusa-config.ts:44) — большинство тестов идёт на in-memory-заменах. rbac — исключение, он вынесен из-под isTest-ветки и не выключается тестовым окружением: иначе checkPermissions 403-ил бы в тестах, поскольку роуты policy-wrap'аются по тому же флагу независимо от того, загружен ли сам модуль (детали — раздел «Архитектура» в CLAUDE.md проекта).

Практическое следствие: зелёные unit- и integration-тесты не проверяют реальные Redis/S3/ Meilisearch/ApiShip — это осознанный компромисс скорости тестов, а не пробел покрытия, который нужно закрывать. Живые прогоны против настоящих сервисов — отдельная задача (см. «Тесты — только на моках» в CLAUDE.md).

Тестовая почта: E2E_RUN​

E2E_RUN=true подменяет email-провайдер на локальный interception endpoint вместо реального SMTP (medusa-config.ts:166) — письма в тестах никуда не улетают наружу. При включённом флаге обязателен E2E_NOTIFICATION_STUB_URL (medusa-config.ts:6) — адрес заглушки. Сама отправка в эту заглушку прерывается через 5 секунд (modules/notification-e2e/service.ts:54).

Event bus на Redis​

Упавшее событие повторяется до 5 раз с экспоненциальной задержкой (база 5 секунд), ошибка хранится до 7 дней (medusa-config.ts:121). Это касается подписчиков (subscribers/*) — если обработчик события бросает исключение, Medusa сам поставит повтор по этому расписанию, без собственного retry-кода в подписчике.

Не все ошибки уходят в этот retry

Некоторые подписчики намеренно логируют ошибку и подавляют её вместо того, чтобы бросить исключение — например письмо сброса пароля (subscribers/send-password-reset-email.ts) или уведомление о возврате (subscribers/shipping-return-notifications.ts:40). Такие ошибки НЕ попадают в event-bus retry выше — это решение конкретного подписчика, а не общее поведение.

Первый администратор​

ADMIN_EMAIL / ADMIN_PASSWORD — из них контейнер backend-init создаёт администратора при первом docker compose up. Значения по умолчанию (admin@test.com / supersecret) публичны: это дефолт из репозитория, а не пароль. Меняйте до того, как стенд станет доступен снаружи.

Почта​

SMTP_HOST, SMTP_PORT (по умолчанию 587), SMTP_USER, SMTP_PASSWORD, SMTP_FROM (modules/notification-smtp/service.ts:42,193). Порт 465 автоматически включает secure-режим; авторизация подключается только если задан SMTP_USER. Отсутствующие host/from не мешают загрузке приложения — ошибка возникает только при попытке отправки (:32). Вложения Medusa Notification передаются в nodemailer вместе с текстовой и HTML-версией письма (:68).

Ошибки отправки логируются и глотаются: недоступный SMTP никогда не ломает сценарий, который письмо породил. Обратная сторона — молчаливое отсутствие писем выглядит как «всё хорошо».

Ссылка сброса пароля заявляет срок действия 15 минут; сбой SMTP при её отправке не ставится в event-bus retry (subscribers/send-password-reset-email.ts). При отсутствии STOREFRONT_URL ссылка отписки от напоминаний о брошенной корзине строится с резервным адресом http://localhost:8000 (lib/abandoned-cart-links.ts:3) — на продакшене без заданного STOREFRONT_URL эта ссылка будет вести не туда.

Хранилище и поиск​

S3/MinIO: S3_BUCKET, S3_FILE_URL, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION, S3_FORCE_PATH_STYLE. У Strapi отдельный бакет и отдельные ключи (STRAPI_S3_*) с доступом только к нему — так загрузки редактора не могут задеть изображения товаров. STRAPI_S3_PUBLIC_URL, STRAPI_S3_ROOT_PATH (по умолчанию media), STRAPI_S3_FORCE_PATH_STYLE (по умолчанию true, в отличие от S3_FORCE_PATH_STYLE у Medusa) задают адресацию отдельно от бакета товаров (apps/cms/config/plugins.ts).

Загрузки в CMS ограничены форматом и размером

Strapi принимает только image/jpeg, image/png, image/webp, image/avif — исполняемые и прочие форматы отклоняются на уровне allowlist. STRAPI_UPLOAD_SIZE_LIMIT ограничивает размер файла, по умолчанию 8 МиБ (plugins.ts:55). Объекты загружаются с ACL public-read.

S3_FORCE_PATH_STYLE=true переключает адресацию объектов с виртуального хоста (bucket.s3.amazonaws.com/key) на path-style (s3.amazonaws.com/bucket/key, medusa-config.ts:111) — MinIO по умолчанию понимает только path-style, поэтому в docker-compose-стенде эта переменная обязательна; для настоящего AWS S3 её обычно не задают.

Meilisearch: MEILI_MASTER_KEY, MEILISEARCH_HOST, MEILISEARCH_API_KEY.

Связь с CMS​

STRAPI_INTERNAL_URL, STRAPI_READ_TOKEN, STRAPI_READ_TOKEN_FILE, CMS_BOOTSTRAP_TOKEN_DIR, STRAPI_WEBHOOK_SECRET, CMS_PREVIEW_INTERNAL_SECRET, CMS_PREVIEW_SIGNING_SECRET — см. Контент из Strapi на витрину.

STRAPI_READ_TOKEN необязателен: это совместимое ручное переопределение для уже настроенных окружений. Если он пуст, backend читает токен из STRAPI_READ_TOKEN_FILE (по умолчанию /run/cms-bootstrap/strapi-read-token), который создаёт одноразовый cms-bootstrap. В Compose путь строится из общего CMS_BOOTSTRAP_TOKEN_DIR, поэтому bootstrap и backend всегда используют один и тот же том; не задавайте STRAPI_READ_TOKEN_FILE там отдельно. Для запуска вне Compose можно задать STRAPI_READ_TOKEN_FILE напрямую. COMPANY_NAME используется только при первом создании Site settings; при отсутствии берётся «Новый магазин». Существующие записи CMS, в том числе черновики, не перезаписываются.

Собственная конфигурация Strapi (apps/cms/config)​

Отдельный набор переменных, независимый от списков выше — читается только процессом CMS:

  • HOST/PORT — по умолчанию 0.0.0.0/1337 (config/server.ts).
  • APP_KEYS — обязателен, список ключей сессии админки Strapi (без дефолта, env.array).
  • Секреты Strapi разделены по назначению, каждый обязателен без дефолта (config/admin.ts): ADMIN_JWT_SECRET (сессии админки), API_TOKEN_SALT (соль для API-токенов, тот же механизм, что создаёт read-токен для backend), TRANSFER_TOKEN_SALT, ENCRYPTION_KEY.
  • STRAPI_JWT_SECRET — отдельный секрет, не путать с ADMIN_JWT_SECRET выше: передаётся CMS как JWT_SECRET плагина users-permissions (docker-compose.yml:267, обязателен) — подписывает токены пользователей плагина, а не сессии Strapi-админки.
  • DATABASE_CLIENT — postgres/mysql/sqlite.
  • WEBHOOKS_POPULATE_RELATIONS (config/server.ts:10, по умолчанию выключено) — включает заполнение relations в payload штатных Strapi-вебхуков (не путать с кастомным вебхуком публикации, см. Контент из Strapi, у него свой формат).
  • FLAG_NPS, FLAG_PROMOTE_EE, FLAG_DOC_LINKS (config/admin.ts:20, по умолчанию все включены) — служебные UI-элементы Strapi Admin (опрос удовлетворённости, промо Enterprise, ссылки на документацию), не влияют на бизнес-логику.
(*) Пустой DATABASE_CLIENT тихо переключает Strapi на SQLite

По умолчанию DATABASE_CLIENT равен sqlite, а не postgres (config/database.ts:7) — файл .tmp/data.db внутри контейнера. Ошибочно не заданная переменная не роняет запуск: Strapi просто поднимется на локальной SQLite вместо ожидаемой PostgreSQL, и весь контент будет жить в эфемерном файле контейнера, а не в БД cms-db-init (см. Docker Compose).

При DATABASE_CLIENT=postgres дополнительно доступны DATABASE_SCHEMA (по умолчанию public), DATABASE_SSL* (по умолчанию выключен) и DATABASE_POOL_MIN/DATABASE_POOL_MAX (по умолчанию 2/10).

Плагин users-permissions (config/plugins.ts:21) настроен на jwtManagement: 'refresh' — токен пользователя обновляется по refresh-механизму, а не выдаётся один раз надолго, и сама refresh-сессия хранится в HttpOnly-cookie (sessions.httpOnly: true), недоступной клиентскому JS.

CSP CMS (config/middlewares.ts) строит connect-src/img-src/media-src на основе origin из STRAPI_S3_PUBLIC_URL — незаданная или неверная переменная блокирует загрузку медиа из MinIO браузером, даже если сама CMS их отдаёт. upgradeInsecureRequests отключён — значимо для локального HTTP-контура без TLS-прокси.

Чего в переменных нет​

Реквизиты МойСклад, ЮKassa, ApiShip и СДЭК в .env не хранятся. Их вводят в админке, они шифруются и лежат в базе. В окружении есть только ключи шифрования.