25. Деплой в Docker
🐳 Деплой Next.js в Docker
Заголовок раздела «🐳 Деплой Next.js в Docker»Представь, что твоё приложение — это сложный механизм: часы с десятками шестерёнок, пружин и колёсиков. Ты собрал его у себя на столе, всё работает идеально. Но стоит передать его другому человеку — что-то идёт не так. Другая версия Node.js, другие переменные окружения, другой Linux. Docker решает эту проблему: ты упаковываешь весь механизм вместе с инструментами, столом и освещением в один ящик — и он работает везде одинаково! 📦
🤔 Зачем Docker для Next.js?
Заголовок раздела «🤔 Зачем Docker для Next.js?»Без Docker │ С Docker─────────────────────────────────────┼──────────────────────────────────────"У меня работает!" 🤷 │ Работает везде одинаково ✅Node.js 18 на сервере, 20 локально │ Одна версия Node.js везде ✅Ручная установка зависимостей │ Всё в образе ✅Сложное масштабирование │ docker scale --replicas=5 ✅Зависимость от ОС сервера │ Изолированный контейнер ✅Нет воспроизводимых сборок │ Один образ = один результат ✅Откат версии — боль │ docker pull myapp:v1.2.3 ✅Docker особенно полезен когда:
- Деплоишь на VPS (DigitalOcean, Hetzner, AWS EC2) — полный контроль
- Нужен Kubernetes — оркестрация контейнеров
- Несколько сервисов — Next.js + PostgreSQL + Redis в одном compose
- CI/CD в компании — единый артефакт для всех окружений
⚠️ output: ‘standalone’ — критически важно!
Заголовок раздела «⚠️ output: ‘standalone’ — критически важно!»Это первое, что нужно добавить в next.config.mjs перед написанием Dockerfile:
/** @type {import('next').NextConfig} */const nextConfig = { output: 'standalone', // ☝️ Без этого Docker-образ будет весить 1+ ГБ! // С этим — 100-200 МБ.
// Почему это работает? // Next.js анализирует, какие файлы из node_modules реально нужны, // и копирует только их. Плюс создаёт self-contained сервер // в .next/standalone/server.js — без npm install!};
export default nextConfig;Что происходит после next build с standalone:
.next/├── standalone/ ← весь нужный код + минимум зависимостей│ ├── server.js ← запускаем это!│ ├── node_modules/ ← только нужные пакеты (не все 847!)│ └── .next/│ └── server/ ← серверные компоненты├── static/ ← статические ассеты (CSS, JS, изображения)└── ...
# С standalone: node_modules/ = ~30 МБ в образе!🏗️ Multi-stage build — секрет маленьких образов
Заголовок раздела «🏗️ Multi-stage build — секрет маленьких образов»Multi-stage build — это когда сборка происходит в несколько этапов, и в финальный образ попадает только то, что нужно для запуска:
Этап 1: deps Устанавливаем зависимости ↓Этап 2: builder Собираем приложение (next build) ↓Этап 3: runner Только то, что нужно для запускаБез multi-stage: в образ попали бы все инструменты сборки, исходники, dev-зависимости. С ним — только production-артефакты.
📄 Полный Dockerfile для Next.js
Заголовок раздела «📄 Полный Dockerfile для Next.js»# Dockerfile
# ─── Этап 1: Установка зависимостей ───────────────────────────────────────────FROM node:20-alpine AS deps
# Устанавливаем libc6-compat для совместимости с некоторыми npm-пакетамиRUN apk add --no-cache libc6-compat
WORKDIR /app
# Копируем файлы с зависимостямиCOPY package.json package-lock.json* ./
# Устанавливаем зависимости (только production в финальном образе)RUN npm ci
# ─── Этап 2: Сборка приложения ────────────────────────────────────────────────FROM node:20-alpine AS builder
WORKDIR /app
# Копируем зависимости из предыдущего этапаCOPY --from=deps /app/node_modules ./node_modules
# Копируем исходный кодCOPY . .
# Переменные окружения для сборки (не секретные!)# Секретные передаются через --build-arg или --secretENV NEXT_TELEMETRY_DISABLED=1ENV NODE_ENV=production
# Сборка Next.jsRUN npm run build
# ─── Этап 3: Production runner ────────────────────────────────────────────────FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=productionENV NEXT_TELEMETRY_DISABLED=1
# Создаём непривилегированного пользователя (безопасность!)RUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjs
# Копируем статические ассетыCOPY --from=builder /app/public ./public
# Настраиваем права для standaloneRUN mkdir .nextRUN chown nextjs:nodejs .next
# Копируем standalone-сервер (с нужными node_modules)COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./# Копируем статику отдельно (она не входит в standalone)COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Переключаемся на непривилегированного пользователяUSER nextjs
# Указываем порт (документация для Docker)EXPOSE 3000
# Переменные окружения для runtimeENV PORT=3000ENV HOSTNAME="0.0.0.0"
# Запуск standalone-сервераCMD ["node", "server.js"]Разберём каждый этап детально:
# Почему node:20-alpine, а не node:20?# alpine — минимальный Linux на базе Alpine (5 МБ vs 900 МБ для Debian)# Итого: node:20-alpine = ~170 МБ, node:20 = ~1 ГБ
# apk add libc6-compat — некоторые npm-пакеты собраны под glibc,# а Alpine использует musl libc. Этот пакет обеспечивает совместимость.
# npm ci vs npm install:# npm ci — строго по lock-файлу, быстрее, идемпотентно# npm install — может обновлять версии, медленнее
# addgroup/adduser — запуск от непривилегированного пользователя# Если контейнер взломают, у хакера не будет root-прав!
# NEXT_TELEMETRY_DISABLED=1 — отключаем телеметрию Next.js в образе🚫 .dockerignore — не копируем лишнее
Заголовок раздела «🚫 .dockerignore — не копируем лишнее»# Зависимости (переустановим внутри контейнера)node_modulesnpm-debug.log*yarn-debug.log*yarn-error.log*
# Сборка Next.js.nextout
# Git.git.gitignore
# ДокументацияREADME.mdCHANGELOG.mddocs/
# Тесты__tests__*.test.ts*.test.tsx*.spec.ts*.spec.tsxcoverage/.nyc_output
# IDE и редакторы.vscode.idea*.swp*.swo
# Переменные окружения (НИКОГДА не копируем!).env.env.local.env.development.local.env.test.local.env.production.local
# CI/CD.github.gitlab-ci.yml
# Локальные скриптыMakefiledocker-compose*.yml
# OS.DS_StoreThumbs.dbБез .dockerignore каждый COPY . . скопирует всё, включая node_modules (500 МБ!), что многократно замедлит сборку.
🔨 Команды для работы с Docker
Заголовок раздела «🔨 Команды для работы с Docker»# ──── Сборка образа ────────────────────────────────────────────────────────────
# Базовая сборкаdocker build -t my-next-app .
# С тегом версииdocker build -t my-next-app:1.0.0 .docker build -t my-next-app:latest .
# С аргументами сборки (для build-time переменных)docker build \ --build-arg NEXT_PUBLIC_API_URL=https://api.my-app.com \ -t my-next-app .
# Посмотреть размер образаdocker image ls my-next-app
# ──── Запуск контейнера ────────────────────────────────────────────────────────
# Базовый запускdocker run -p 3000:3000 my-next-app
# С переменными окруженияdocker run \ -p 3000:3000 \ -e DATABASE_URL=postgresql://user:pass@host:5432/db \ -e NEXTAUTH_SECRET=my-secret \ my-next-app
# Из .env файлаdocker run \ -p 3000:3000 \ --env-file .env.production \ my-next-app
# В фонеdocker run -d \ --name my-next-app \ -p 3000:3000 \ --restart unless-stopped \ --env-file .env.production \ my-next-app
# ──── Управление ───────────────────────────────────────────────────────────────
# Статус контейнеровdocker psdocker ps -a # включая остановленные
# Логиdocker logs my-next-appdocker logs -f my-next-app # следить в реальном времени
# Зайти внутрь контейнераdocker exec -it my-next-app sh
# Остановить/удалитьdocker stop my-next-appdocker rm my-next-app
# Очистка (удаляет неиспользуемые образы, контейнеры, сети)docker system prune -a🌍 Переменные окружения в Docker
Заголовок раздела «🌍 Переменные окружения в Docker»В Docker есть несколько способов передавать переменные:
# Способ 1: Флаг -e (одна переменная)docker run -e DATABASE_URL=postgresql://... my-next-app
# Способ 2: --env-file (файл с переменными)docker run --env-file .env.production my-next-app
# Способ 3: В docker-compose.yml (рекомендуется для разработки)# см. секцию ниже
# Способ 4: Docker Secrets (для production!)echo "postgresql://user:password@db:5432/mydb" | docker secret create db_url -docker service create \ --secret db_url \ --name my-next-app \ my-next-app:latest# .env.production — НЕ коммитить в Git!NEXTAUTH_SECRET=very-long-random-string-at-least-32-charactersNEXTAUTH_URL=https://my-app.comSTRIPE_SECRET_KEY=sk_live_...REDIS_URL=redis://redis:6379Важно: переменные, начинающиеся с NEXT_PUBLIC_, встраиваются в сборку во время next build. Их нельзя изменить в runtime! Для runtime-переменных используй серверные переменные окружения.
🎵 docker-compose для локальной разработки
Заголовок раздела «🎵 docker-compose для локальной разработки»version: '3.8'
services: app: build: context: . dockerfile: Dockerfile ports: - '3000:3000' environment: DATABASE_URL: postgresql://postgres:password@db:5432/mydb REDIS_URL: redis://redis:6379 NEXTAUTH_SECRET: local-dev-secret-not-for-production NEXTAUTH_URL: http://localhost:3000 depends_on: db: condition: service_healthy redis: condition: service_started restart: unless-stopped
db: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: mydb ports: - '5432:5432' volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 10s timeout: 5s retries: 5
redis: image: redis:7-alpine ports: - '6379:6379' volumes: - redis_data:/data command: redis-server --appendonly yes
volumes: postgres_data: redis_data:# Запустить всё одной командойdocker compose up -d
# Посмотреть логи всех сервисовdocker compose logs -f
# Пересобрать образ (после изменений в коде)docker compose up -d --build
# Остановитьdocker compose down
# Удалить вместе с даннымиdocker compose down -v🌐 Nginx как reverse proxy
Заголовок раздела «🌐 Nginx как reverse proxy»Nginx стоит перед Next.js и занимается:
- Терминацией SSL
- Кэшированием статики
- Rate limiting
- Балансировкой нагрузки
events { worker_connections 1024;}
http { # Кэширование статики proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=7d use_temp_path=off;
upstream nextjs { server app:3000; # Для нескольких инстансов: # server app1:3000; # server app2:3000; # server app3:3000; }
server { listen 80; server_name my-app.com www.my-app.com;
# Редирект на HTTPS return 301 https://$host$request_uri; }
server { listen 443 ssl; server_name my-app.com www.my-app.com;
ssl_certificate /etc/letsencrypt/live/my-app.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/my-app.com/privkey.pem;
# Заголовки безопасности add_header X-Frame-Options SAMEORIGIN; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header Strict-Transport-Security "max-age=31536000" always;
# Статические ассеты Next.js — кэшируем агрессивно location /_next/static { proxy_cache STATIC; proxy_pass http://nextjs; add_header Cache-Control "public, max-age=31536000, immutable"; }
location /static { proxy_cache STATIC; proxy_pass http://nextjs; add_header Cache-Control "public, max-age=31536000, immutable"; }
# Всё остальное — проксируем в Next.js location / { proxy_pass http://nextjs; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; 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_cache_bypass $http_upgrade; } }}# docker-compose с Nginx + Certbot (Let's Encrypt)version: '3.8'
services: nginx: image: nginx:alpine ports: - '80:80' - '443:443' volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - certbot_certs:/etc/letsencrypt:ro - certbot_www:/var/www/certbot:ro depends_on: - app restart: unless-stopped
certbot: image: certbot/certbot volumes: - certbot_certs:/etc/letsencrypt - certbot_www:/var/www/certbot command: certonly --webroot --webroot-path=/var/www/certbot --email [email protected] --agree-tos --no-eff-email -d my-app.com -d www.my-app.com
app: build: . environment: DATABASE_URL: postgresql://postgres:password@db:5432/mydb
db: image: postgres:16-alpine # ...
volumes: certbot_certs: certbot_www:💊 Health Checks
Заголовок раздела «💊 Health Checks»Health checks позволяют Docker знать, что контейнер действительно работает (не просто запущен):
# Добавляем в Dockerfile перед CMDHEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD wget -qO- http://localhost:3000/api/health || exit 1// app/api/health/route.ts — эндпоинт для health checkimport { NextResponse } from 'next/server';
export async function GET() { try { // Можно проверить подключение к БД // await db.query('SELECT 1');
return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), }); } catch (error) { return NextResponse.json( { status: 'error', error: String(error) }, { status: 503 } ); }}# Health check в docker-composeservices: app: build: . healthcheck: test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/api/health'] interval: 30s timeout: 10s retries: 3 start_period: 40s📏 Оптимизация размера образа
Заголовок раздела «📏 Оптимизация размера образа»# ✅ Оптимизированный Dockerfile — приёмы:
# 1. Используй alpine вместо fullFROM node:20-alpine # ~170 МБ vs ~1 ГБ для debian
# 2. Один RUN вместо многих (меньше слоёв)# ❌ Плохо:RUN apk add --no-cache curlRUN apk add --no-cache gitRUN apk add --no-cache bash# ✅ Хорошо:RUN apk add --no-cache curl git bash
# 3. Очищай кэш пакетного менеджераRUN apk add --no-cache curl \ && rm -rf /var/cache/apk/*
# 4. Копируй только нужные файлыCOPY package.json package-lock.json ./ # Сначала зависимостиRUN npm ci # УстанавливаемCOPY . . # Потом исходники
# 5. Multi-stage build (уже обсудили)
# 6. .dockerignore — не копируй node_modules и .nextПример анализа размеров:
# Просмотр слоёв образаdocker history my-next-app
# Анализ образа утилитой divedocker run --rm -it \ -v /var/run/docker.sock:/var/run/docker.sock \ wagoodman/dive:latest my-next-app
# Размеры наших образов:# Без standalone + без multi-stage: 1.2 ГБ 😱# Без standalone + с multi-stage: 600 МБ# Со standalone + с multi-stage: 150 МБ ✅# Со standalone + distroless: 95 МБ ✅✅🖥️ Деплой на VPS (DigitalOcean / Hetzner)
Заголовок раздела «🖥️ Деплой на VPS (DigitalOcean / Hetzner)»# На локальной машине: собираем и пушим образdocker build -t username/my-next-app:latest .docker push username/my-next-app:latest
# На сервере: тянем и запускаемssh user@your-vps-ip
# Устанавливаем Docker (один раз)curl -fsSL https://get.docker.com | shsudo usermod -aG docker $USER
# Создаём файл с переменными окруженияnano /home/user/app/.env.production# DATABASE_URL=...# NEXTAUTH_SECRET=...
# Запускаем контейнерdocker run -d \ --name my-next-app \ --restart unless-stopped \ -p 3000:3000 \ --env-file /home/user/app/.env.production \ username/my-next-app:latest
# Обновление приложения (zero-downtime с Nginx)docker pull username/my-next-app:latestdocker stop my-next-appdocker rm my-next-appdocker run -d \ --name my-next-app \ --restart unless-stopped \ -p 3000:3000 \ --env-file /home/user/app/.env.production \ username/my-next-app:latest⚙️ GitHub Actions — CI/CD для Docker
Заголовок раздела «⚙️ GitHub Actions — CI/CD для Docker»name: Build and Deploy
on: push: branches: [main] tags: ['v*']
env: REGISTRY: docker.io IMAGE_NAME: username/my-next-app
jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write
steps: - name: Checkout uses: actions/checkout@v4
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix=sha-
- name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max
deploy: needs: build-and-push runs-on: ubuntu-latest if: github.ref == 'refs/heads/main'
steps: - name: Deploy to VPS uses: appleboy/ssh-action@v1 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} script: | docker pull username/my-next-app:main docker stop my-next-app || true docker rm my-next-app || true docker run -d \ --name my-next-app \ --restart unless-stopped \ -p 3000:3000 \ --env-file /home/deploy/app/.env.production \ username/my-next-app:main docker image prune -f☸️ Kubernetes — основы для Next.js
Заголовок раздела «☸️ Kubernetes — основы для Next.js»Kubernetes (k8s) — это оркестратор контейнеров. Когда приложение достаточно большое, что одного Docker-compose не хватает:
apiVersion: apps/v1kind: Deploymentmetadata: name: next-app namespace: productionspec: replicas: 3 selector: matchLabels: app: next-app template: metadata: labels: app: next-app spec: containers: - name: next-app image: username/my-next-app:v1.0.0 ports: - containerPort: 3000 env: - name: NODE_ENV value: production - name: DATABASE_URL valueFrom: secretKeyRef: name: app-secrets key: database-url resources: requests: cpu: '250m' memory: '256Mi' limits: cpu: '500m' memory: '512Mi' livenessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 5 periodSeconds: 5apiVersion: v1kind: Servicemetadata: name: next-app-servicespec: selector: app: next-app ports: - port: 80 targetPort: 3000 type: ClusterIP
---# k8s/ingress.yamlapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: next-app-ingress annotations: cert-manager.io/cluster-issuer: letsencrypt-prodspec: rules: - host: my-app.com http: paths: - path: / pathType: Prefix backend: service: name: next-app-service port: number: 80 tls: - hosts: - my-app.com secretName: next-app-tls# Базовые команды kubectlkubectl apply -f k8s/kubectl get pods -n productionkubectl get deployments -n productionkubectl rollout status deployment/next-app -n production
# Rolling update (zero-downtime!)kubectl set image deployment/next-app \ next-app=username/my-next-app:v1.1.0 \ -n production
# Откатkubectl rollout undo deployment/next-app -n production
# Масштабированиеkubectl scale deployment/next-app --replicas=5 -n production🎯 Итог: какой способ выбрать?
Заголовок раздела «🎯 Итог: какой способ выбрать?»Размер проекта │ Рекомендуется──────────────────┼──────────────────────────────────────────Хобби / MVP │ Vercel (бесплатно, zero-config)Стартап │ Vercel Pro ИЛИ Docker + DigitalOceanСредний бизнес │ Docker + docker-compose + VPSКорпорация │ Docker + Kubernetes (GKE/EKS/AKS)Главные правила Docker для Next.js:
- Всегда
output: 'standalone'вnext.config.mjs - Multi-stage build — builder → runner
- Alpine-образы — меньше размер
- Непривилегированный пользователь — безопасность
- Health checks — Docker знает когда контейнер готов
- .dockerignore — не копируй лишнее