Что случилось и почему это важно
В декабре 2025 года мир Next.js и React потрясла серия критических уязвимостей. Если Вы используете Next.js - эта статья для Вас.
CVE-2025-55182 (React2Shell)
Уровень опасности: CVSS 10.0 из 10.0 (максимальный)
Это уязвимость в React Server Components. Злоумышленник может Выполнить любой код на Вашем сервере, просто отправив специально сформированный HTTP-запрос. Без пароля, без авторизации - просто запрос, и Ваш сервер взломан.
Что затронуто:
- react-server-dom-* версий 19.0.0, 19.1.0, 19.1.1, 19.2.0
- Next.js 15.x и 16.x (App Router)
- Next.js начиная с версии 14.3.0-canary.77
Исправлено в:
- React: 19.0.1, 19.1.2, 19.2.1
- Next.js: 14.x stable, 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7
CVE-2025-66478
Это та же уязвимость, но специфичная для Next.js. По сути — дубликат CVE-2025-55182, но для понимания: если вы на Next.js с App Router - Вы были уязвимы.
CVE-2025-55183 (Source Code Exposure)
Уровень опасности: Средний
Позволяет получить исходный код Ваших Server Actions. То есть злоумышленник может увидеть Вашу бизнес-логику. Если Вы случайно захардкодили пароль в коде - его тоже увидят.
Исправлено в: Next.js 15.0.7+, 15.1.11+, 15.2.8+, 15.3.8+, 15.4.10+, 15.5.9+, 16.0.10+
Версии 14.x и 13.x не получили исправление этой уязвимости.
CVE-2025-55184 (Denial of Service)
Уровень опасности: Высокий
Denial of Service (DoS) - специальный запрос вешает Ваш сервер, и он перестаёт отвечать на любые запросы. Сайт просто падает.
Исправлено в: Next.js 13.3+, 14.2.35+, 15.0.7+, 15.1.11+, 15.2.8+, 15.3.8+, 15.4.10+, 15.5.9+, 16.0.10+
Зачем переносить в Docker?
Вы спросите: "Я обновил Next.js, уязвимости закрыты. Зачем мне Docker?"
Отличный вопрос. Вот зачем:
- Изоляция - если завтра найдут новую уязвимость и Ваш сервер взломают, злоумышленник окажется внутри контейнера, а не на Вашем хосте. Это как комната внутри комнаты.
- Минимум лишнего - в контейнере только то, что нужно для работы приложения. Меньше софта = меньше потенциальных дыр.
- Быстрое обновление - нашли уязвимость? Пересобрали контейнер, перезапустили. Всё. Не нужно SSH на сервер и ручные танцы с бубном.
- Одинаковое окружение - "у меня локально работает" больше не аргумент. В контейнере всё одинаково: и у Вас, и на сервере.
Как сейчас выглядит Ваш проект (с PM2)
Допустим, у Вас типичная ситуация:

И Вы запускаете это так:
npm run build
pm2 start ecosystem.config.js
Ваш ecosystem.config.js выглядит примерно так:
module.exports = {
apps: [{
name: 'my-app',
script: 'node_modules/next/dist/bin/next',
args: 'start',
instances: 2,
exec_mode: 'cluster'
}]
}
Сейчас мы шаг за шагом перенесём это в Docker.
Шаг 1: Обновите Next.js до безопасной версии
Прежде чем что-то контейнеризировать, убедитесь что у Вас безопасная версия:
Проверяем текущую версию
npm list next
Обновляем до последней безопасной версии
npm install next@latest react@latest react-dom@latest
Безопасные версии (исправлены все уязвимости):
| Ветка | Минимальная безопасная версия |
|---|---|
| 16.0.x | 16.0.10 |
| 15.5.x | 15.5.9 |
| 15.4.x | 15.4.10 |
| 15.3.x | 15.3.8 |
| 15.2.x | 15.2.8 |
| 15.1.x | 15.1.11 |
| 15.0.x | 15.0.7 |
| 14.x | 14.2.35 (только DoS-фикс) |
React (react-server-dom-*): 19.0.1, 19.1.2, 19.2.1
Важно: Версии 14.x получили только исправление DoS (CVE-2025-55184). Уязвимость утечки исходного кода (CVE-2025-55183) в них не исправлена. Рекомендуется обновиться до 15.x.
Шаг 2: Настройте next.config.js для Docker
Откройте Ваш next.config.js и добавьте одну важную строку: output: 'standalone',, пример next.config.js:

Что делает output: 'standalone'?
Когда Вы запускаете npm run build, Next.js создаст папку .next/standalone с минимальным набором файлов для запуска. Это уменьшит размер Docker-образа в 3-5 раз.
После изменения пересоберите проект:
npm run build
Проверьте, что появилась папка .next/standalone:
ls -la .next/standalone/
Вы должны увидеть файл server.js - это и есть Ваше приложение в одном файле.
Шаг 3: Создайте файл .dockerignore
Создайте файл .dockerignore в корне проекта. Он говорит Docker, что НЕ копировать в образ:
node_modules
.env
.env.local
.env.development
.env.production.local
.git
.gitignore
.vscode
.idea
ecosystem.config.js
.pm2
*.log
npm-debug.log*
__tests__
*.test.js
*.test.ts
coverage
.next
.DS_Store
Разберем указаные директории, файлы:
node_modules - зависимости, которые установим внутри контейнера
.env* - локальные env-файлы с паролями не должны попасть внутрь образа
.git, .gitignore, .vscode, .idea, .DS_Store - git, редакторы, MacOS
ecosystem.config.js, .pm2 - pm2 нам больше не нужен
*.log, npm-debug.log* - логи
__tests__, *.test.js, *.test.ts, coverage - тесты
.next - результаты сборки (соберем внутри контейнера)
Почему это важно?
Если не создать .dockerignore, в Ваш образ попадут все файлы, включая .env с паролями. Любой, кто получит доступ к образу, увидит Ваши секреты.
Шаг 4: Создайте Dockerfile
Создайте файл Dockerfile (без расширения) в корне проекта:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm ci
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN apk add --no-cache dumb-init
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["dumb-init", "node", "server.js"]
Что тут происходит?
Мы используем multi-stage build - три этапа сборки:
- deps - только устанавливаем зависимости
- builder - собираем приложение
- runner - финальный образ только с тем, что нужно для запуска
В финальный образ НЕ попадают:
- Исходный код (только скомпилированный)
- devDependencies
- Файлы сборки
Это и безопаснее, и образ получается маленький (примерно 150-200 МБ вместо 1+ ГБ).
Также в этом Dockerfile мы используем Alpine образы, которые являються минималистичным Linux, поскольку чем меньше размер изначального образа тем меньше уязвимостей. Также устанавливаем dumb-init, поскольку Node.js плохо работает как главный процесс (PID 1) в контейнере, а dumb-init решает эту проблему. Используем отдельного пользователя чтобы наше приложение не работало от root пользователя.
Шаг 5: Создайте docker-compose.yml
Docker Compose упрощает запуск. Создайте файл docker-compose.yml:
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: my-nextjs-app
restart: unless-stopped
ports:
- "3000:3000"
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
environment:
- NODE_ENV=production
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Раздел лимитов (
deploy: resources: limits:) желательно изменить под мощность Вашего сервера.
В этом docker-compose.yml мы собираем образ из текущей директории, указываем порт доступа к проекта - 3000 (при необходимости можна изменить, досттаочно просто изменить первую цифру 3000), также ограничиваем ресурсы (это не обязательно, но рекомендуеться), запрещаем повышение привилегий и делаем проверку здоровя приложения.
Шаг 6: Собираем и запускаем
Теперь у Вас должна быть такая структура:

ecosystem.config.js можно удалить, поскольку он больше не нужен.
Собираем образ:
docker compose build
Первая сборка займёт несколько минут. Последующие будут быстрее благодаря кэшированию.
Запускаем:
docker compose up -d
Флаг -d запускает в фоновом режиме (как pm2).
Проверяем что работает:
Смотрим статус:
docker compose psСмотрим логи:
docker compose logs -fПроверяем в браузере:
curl http://localhost:3000
Пример корректной работы:

Шаг 7: Основные команды (вместо PM2)
Вот шпаргалка. Слева - как было с PM2, справа - как теперь с Docker:
| Действие | PM2 | Docker Compose |
|---|---|---|
| Запустить | pm2 start ecosystem.config.js | docker compose up -d |
| Остановить | pm2 stop all | docker compose down |
| Перезапустить | pm2 restart all | docker compose restart |
| Посмотреть логи | pm2 logs | docker compose logs -f |
| Статус | pm2 status | docker compose ps |
| Пересобрать после изменений | npm run build && pm2 restart all | docker compose up -d --build |
Шаг 8: Переменные окружения (секреты)
Важно: Никогда не храните пароли в Dockerfile или docker-compose.yml!
Создайте файл .env.production (добавьте его в .gitignore!):
DATABASE_URL=postgresql://user:password@host:5432/db
API_SECRET=your-secret-key
Обновите docker-compose.yml:
services:
app:
# ... остальное как было ...
env_file:
- .env.production
Шаг 9: Запуск на сервере
На Вашем сервере:
1. Установите Docker (Ubuntu/Debian):
apt-get update && apt-get install ca-certificates curl && \
install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \
chmod a+r /etc/apt/keyrings/docker.asc && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
apt-get update && \
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
2. Скопируйте проект на сервер (без node_modules и .next):
rsync -avz --exclude 'node_modules' --exclude '.next' --exclude '.git' \
./ user@server:/path/to/app/
3. Скопируйте .env.production отдельно:
scp .env.production user@server:/path/to/app/
4. На сервере:
cd /path/to/app
docker compose up -d --build
5. Удалите PM2 (если был установлен глобально):
pm2 kill
pm2 unstartup
npm uninstall -g pm2
Что делать при обновлении приложения
- Внесите изменения в код
- Пересоберите и перезапустите:
docker compose up -d --build
Docker пересоберёт только изменившиеся слои, это быстро.
Что делать при выходе новых уязвимостей
1. Обновите зависимости:
npm update next react react-dom
2. Пересоберите образ:
docker compose build --no-cache
docker compose up -d
Флаг --no-cache заставляет пересобрать всё с нуля, включая базовый образ node:20-alpine.
Частые проблемы и решения
"Приложение не стартует"
Смотрите логи:
docker compose logs app
"Не могу подключиться к базе данных"
Если база данных на том же сервере, используйте host.docker.internal вместо localhost:
DATABASE_URL=postgresql://user:pass@host.docker.internal:5432/db
Или добавьте в docker-compose.yml:
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway"
"Образ слишком большой"
Проверьте что .dockerignore создан и содержит node_modules.
Посмотреть размер образа
docker images | grep nextjs
Нормальный размер: 150-300 МБ. Если больше 500 МБ - что-то не так.
"После пересборки изменения не применились"
Используйте флаг --build:
docker compose up -d --build
Если у Вас уже есть Nginx
Если на сервере уже крутится Nginx как reverse proxy (например, для SSL), не открывайте порт 3000 наружу.
Измените в docker-compose.yml секцию ports:
services:
app:
# ... остальное как было ...
# БЫЛО (доступно всем):
# ports:
# - "3000:3000"
# СТАЛО (только для localhost):
ports:
- "127.0.0.1:3000:3000"
Теперь контейнер доступен только с этого же сервера (localhost), а не из интернета.
В конфиге Nginx добавьте proxy_pass:
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
}
}
Необходимо изменить домен на Ваш.
Перезапустите Nginx:
nginx -t && systemctl reload nginx
Заключение
Вы перенесли проект из PM2 в Docker. Теперь у Вас:
- Изолированное окружение
- Приложение работает от непривилегированного пользователя
- Минимальный образ без лишнего софта
- Простое обновление одной командой
- Одинаковое поведение локально и на сервере
При появлении новых уязвимостей вроде CVE-2025-55182 Вам достаточно обновить зависимости и пересобрать образ. Даже если злоумышленник найдёт новую 0-day уязвимость, он окажется внутри контейнера(где будет минимум доступных команд для закрепления и експлуатации сервера), а не на Вашем хосте.