React-приложение в интернете — это после сборки обычный набор статических файлов: JS, CSS, index.html. Но у React есть две ловушки, из-за которых простой деплой превращается в вечер отладки: значения переменных с префиксом REACT_APP_ запекаются в бандл на этапе сборки, а React Router в history mode требует, чтобы сервер возвращал index.html для любого адреса. Ниже — путь от папки с кодом до рабочей HTTPS-ссылки на Dockbox.
Почему деплой React упирается в шаги вокруг сборки
Привычный сценарий такой: собрать проект командой, скопировать папку build (или dist) на хостинг, настроить веб-сервер так, чтобы вложенные адреса не отдавали 404, и отдельно позаботиться о сертификате. Сам React тут ни при чём — сложность в шагах вокруг.
Сборка перед каждой загрузкой. Браузер не исполняет JSX, ему нужны готовые файлы, поэтому деплой всегда начинается с npm run build.
SPA-fallback. Обновление страницы на /about отдаёт 404, хотя переход по ссылке внутри приложения работает.
Переменные REACT_APP_. Их значения подставляются в бандл при сборке и остаются там навсегда — смена переменной в панели сама по себе ничего не изменит.
Статика или SSR. Create React App и Vite собирают статику, Next.js — Node-приложение. Это два разных режима запуска.
Сервер и сертификат. Нужен веб-сервер, выпуск и продление сертификата, автозапуск после падения.
Платформа деплоя берёт эту часть на себя: вы отдаёте исходники, она по package.json понимает, что это React, ставит зависимости, собирает проект и отдаёт публичную HTTPS-ссылку. Ни nginx, ни ручных правил rewrite.
Шаг 1. Убедитесь, что проект собирается локально
В корне проекта должен лежать package.json со скриптом build, и сборка должна проходить на вашей машине:
npm install
npm run build
Дальше всё определяется содержимым package.json, а не инструментом: Create React App кладёт результат в build, Vite — в dist, и от этого зависит только имя папки. Если сборка падает локально, она упадёт и на платформе по той же причине — сначала почините её, потом загружайте код.
Шаг 2. Отдайте исходники, а не node_modules и build
Сборку платформа делает сама, поэтому node_modules и готовый build только мешают: они тяжёлые и содержат пути с вашего компьютера. Загружайте исходный код — тот же набор файлов, что лежит в репозитории. Важно, чтобы среди них был файл блокировки зависимостей (package-lock.json, yarn.lock или pnpm-lock.yaml): по нему версии пакетов встанут те же, что и у вас. Если импортируете проект из GitHub, чистить вообще ничего не нужно.
Плохой вариант:
build/
node_modules/
Правильный — package.json и lock-файл в архиве, остальное восстанавливается из них.
Шаг 3. Переменные REACT_APP_ — главная ловушка
В Create React App в код подставляются только переменные с префиксом REACT_APP_, остальные сборка молча игнорирует. В проекте на Vite префикс другой — VITE_, и работает это так же. Значение попадает в готовый бандл в момент сборки, поэтому его видно любому посетителю в исходнике страницы.
REACT_APP_API_URL=https://api.example.com
Что можно класть: адрес API, название сайта, публичный ключ карт. Что нельзя: токен базы, ключ платёжного провайдера, секрет платного API. Настоящие секреты живут на бэкенде, который возвращает приложению только результат. Вариант «фронтенд ходит в базу напрямую» тоже не годится: в браузере негде спрятать её пароль.
Шаг 4. React Router и правило для любого URL
С React Router в history mode ссылки выглядят обычно: /about, /projects/1. Проблема в том, что при прямом заходе или обновлении страницы сервер ищет файл по этому пути и не находит его — React рисует все страницы сам из index.html. На обычном хостинге для этого пишут правило rewrite; без него вы получаете 404 на /about.
На стороне платформы этот fallback уже настроен: открывайте вложенные адреса, обновляйте страницу — отдаётся тот же index.html. Поэтому переезжать на HashRouter с решёткой в URL не нужно, BrowserRouter работает как есть.
Шаг 5. Разберитесь, статика у вас или SSR
Откройте package.json. Если в зависимостях react-scripts, vite или обычные react и react-dom, а результат сборки — папка с готовыми файлами, у вас статическое SPA: его отдаёт любой веб-сервер. Если в зависимостях next, это серверное приложение: оно само отвечает на запросы, и ему нужны скрипт запуска и порт из окружения.
"scripts": {
"build": "next build",
"start": "next start"
}
Для SSR-варианта приложение должно слушать порт из переменной окружения PORT и адрес 0.0.0.0, а не localhost — иначе платформа до него не дотянется.
Шаг 6. Загрузите код и получите ссылку
Проект попадает на платформу архивом или импортом репозитория GitHub, включая приватный. Ставить Node.js локально и запускать сборку вручную не нужно. Прогресс и логи сборки видны в панели в реальном времени, поэтому ошибка проявляется сразу, а не пустым белым экраном, а после успешного старта вы получаете публичную HTTPS-ссылку.
Сколько это стоит и как работают контейнеры
Здесь принципиальная разница с VPS: администрировать сервер не нужно. Контейнеры работают постоянно, без ограничения по часам и без «засыпания». Упавший контейнер платформа перезапускает сама, и приложение возвращается в строй без вашего участия.
Starter — 990 ₽/мес. 1 vCPU, 2 GB RAM, один проект. Первый месяц бесплатный. Для одного React-приложения этого достаточно, и лимита по времени работы у тарифа нет.
Pro — 2 490 ₽/мес. 2 vCPU, 4 GB RAM, до пяти проектов.
Team — 4 990 ₽/мес — для команд, которым нужен запас ресурсов и SLA.
Свой Dockerfile поддерживается, но не обязателен: стек платформа определяет по коду сама. Если Dockerfile нужен, собирайте бандл отдельной стадией и отдавайте готовую статику лёгким nginx:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
Важная деталь: переменные окружения на этапе запуска на уже собранный бандл не влияют — значения REACT_APP_ должны быть заданы до npm run build.
Частые вопросы
Нужно ли собирать проект самому и загружать папку build?
Нет, достаточно исходного кода. Платформа ставит зависимости и запускает скрипт сборки из package.json, а готовые файлы отдаёт по вашей ссылке. Загрузка собственного build только усложняет обновления: каждую новую версию пришлось бы собирать вручную.
Почему при обновлении страницы /about получается 404?
Так ведёт себя обычный хостинг: он ищет файл по этому пути и ничего не находит, потому что React рисует страницы сам из index.html. Лечится правилом, которое возвращает index.html для любого URL. На нашей стороне оно уже настроено, настраивать ничего не нужно.
Почему после смены переменной REACT_APP_ приложение работает по-старому?
Потому что значение запекается в бандл в момент сборки, а не читается при запуске. Новое значение попадёт в код только после новой сборки — на платформе достаточно загрузить обновлённый код, и сборка запустится заново. Пока контейнер работает, подмена переменной в панели ничего не изменит.
Можно ли положить ключ API в переменную REACT_APP_?
Только если это не настоящий секрет. Значения REACT_APP_-переменных компилируются в готовые файлы, и любой посетитель видит их в исходнике страницы. Ключи, которые открывают доступ к деньгам или приватным данным, должны оставаться на бэкенде.
Проект на Next.js тоже опубликуется?
Да, но это уже не статика: Next.js собирает Node-приложение, которое отвечает на запросы. Платформа определит это по зависимостям и запустит сервис, слушающий порт из переменной PORT. Убедитесь, что в package.json есть скрипт запуска.