Next.js в Docker

Production-ready Dockerfile для Next.js (App Router / Pages Router). Финальный образ: ~150MB, non-root, standalone mode, multi-stage.

Быстрый старт

# Скопируй 3 файла (Dockerfile, compose.yaml, .dockerignore) в корень проекта
docker compose up
# → http://localhost:3000

Подготовка: next.config.js

Добавь output: 'standalone' — Next.js создаст автономный сервер, который не тянет за собой весь node_modules:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}
 
module.exports = nextConfig

Dockerfile

# ─── Stage 1: Установка зависимостей ─────────────────────────
FROM node:20-alpine AS deps
# alpine (~50MB) вместо debian (~350MB)
# Зависимость libc6-compat нужна для некоторых npm-пакетов на Alpine
RUN apk add --no-cache libc6-compat
WORKDIR /app
 
# Копируем ТОЛЬКО файлы зависимостей → кэш слоя сохраняется
# при изменении кода приложения
COPY package.json package-lock.json ./
RUN npm ci
# npm ci вместо npm install:
# - строго следует package-lock.json (воспроизводимая сборка)
# - быстрее (не обновляет lock-файл)
# - удаляет node_modules перед установкой
 
# ─── Stage 2: Сборка приложения ──────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
 
# Копируем зависимости из предыдущего стейджа
COPY --from=deps /app/node_modules ./node_modules
COPY . .
 
# Переменные окружения для сборки
# NEXT_TELEMETRY_DISABLED — отключаем телеметрию Vercel
ENV NEXT_TELEMETRY_DISABLED=1
 
RUN npm run build
# next build создаёт .next/standalone — автономный сервер
# Размер standalone: ~30MB vs ~300MB полного node_modules
 
# ─── Stage 3: Production runtime ─────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
 
# Production-переменные
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
 
# Non-root пользователь (безопасность)
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs
 
# Копируем только необходимое из builder:
# 1. public/ — статические файлы (не включены в standalone)
COPY --from=builder /app/public ./public
 
# 2. standalone — автономный сервер + минимальные node_modules
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
 
# 3. static — собранные CSS/JS бандлы (не включены в standalone)
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
 
USER nextjs
 
EXPOSE 3000
ENV PORT=3000
# hostname 0.0.0.0 обязателен для работы внутри контейнера
ENV HOSTNAME="0.0.0.0"
 
# standalone сервер — один файл server.js
CMD ["node", "server.js"]

compose.yaml — development

services:
  app:
    build:
      context: .
      target: deps              # останавливаемся на стейдже deps
    command: npm run dev        # Next.js dev server с HMR
    ports:
      - "3000:3000"
    volumes:
      - .:/app                  # live reload — изменения в коде видны сразу
      - /app/node_modules       # anonymous volume — не перезаписывать node_modules с хоста
      - /app/.next              # anonymous volume — кэш сборки внутри контейнера
    environment:
      - NODE_ENV=development
      - WATCHPACK_POLLING=true  # hot reload в Docker (inotify не работает через bind mount)

compose.yaml — production

services:
  app:
    build:
      context: .
      target: runner           # финальный production стейдж
    ports:
      - "127.0.0.1:3000:3000" # только localhost (Nginx проксирует снаружи)
    read_only: true
    tmpfs:
      - /app/.next/cache:size=200m  # кэш ISR/image optimization
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 5s
      start_period: 15s
      retries: 3
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

.dockerignore

node_modules
.next
.git
.gitignore
README.md
Dockerfile
docker-compose*.yml
compose*.yaml
.env*.local
.vscode
.idea
*.md

Почему именно так

Standalone output

Без output: 'standalone' в образ попадает весь node_modules (~300MB). Standalone трейсит зависимости и создаёт минимальный сервер (~30MB). Это ключевая настройка.

Три стейджа, а не два

  • deps — устанавливаем зависимости (кэшируется отдельно от кода)
  • builder — собираем приложение (пересобирается при изменении кода, но зависимости берутся из кэша)
  • runner — только runtime (нет npm, нет исходников, нет devDependencies)

COPY public и static отдельно

Standalone НЕ включает public/ и .next/static/. Без этих строк:

  • Не загрузятся шрифты, favicon, изображения из public/
  • Не загрузятся CSS и JS бандлы

HOSTNAME=“0.0.0.0”

По умолчанию Next.js слушает localhost, что внутри контейнера означает только loopback-интерфейс. Docker перенаправляет трафик на IP контейнера, а не на localhost. Без этой переменной приложение недоступно снаружи контейнера.

WATCHPACK_POLLING для dev

Файловая система Docker (bind mount) не поддерживает inotify на macOS/Windows. Без polling hot reload не работает.

Типичные ошибки

ОшибкаСимптомРешение
Нет output: 'standalone' в next.config.jsОбраз 1.5GB, или server.js не найденДобавить output: 'standalone'
Не скопирован public/404 на favicon, шрифты, изображенияCOPY --from=builder /app/public ./public
Не скопирован .next/staticСтраница без CSS/JS (белый экран)COPY --from=builder /app/.next/static ./.next/static
Нет HOSTNAME="0.0.0.0"curl: (7) Failed to connectДобавить ENV HOSTNAME="0.0.0.0"
NEXT_PUBLIC_* не видны в runtimeПеременные undefined в браузереNEXT_PUBLIC_* инлайнятся при сборке — передавать как --build-arg
Sharp не работает на AlpineОшибка при image optimizationRUN npm install --os=linux --cpu=arm64 sharp (для arm) или отключить image optimization
Hot reload не работает в devИзменения не видны без рестартаДобавить WATCHPACK_POLLING=true

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

Next.js различает два типа переменных:

# Build-time (вшиваются в бандл при сборке)
# Для NEXT_PUBLIC_* — передавать через --build-arg
build:
  args:
    - NEXT_PUBLIC_API_URL=https://api.example.com
 
# Runtime (доступны только на сервере)
environment:
  - DATABASE_URL=postgresql://...
  - API_SECRET=...

Варианты

  • С Nginx: поставить nginx-reverse-proxy перед Next.js для SSL и кэширования
  • Полный стек: fullstack — Next.js + API + PostgreSQL + Redis
  • Monorepo (Turborepo): добавить turbo prune перед npm ci в стейдже deps