Как развернуть Next.js на VPS с Docker: полное руководство

Пошаговое руководство по деплою Next.js приложения на VPS с использованием Docker, docker-compose и nginx. SSL-сертификаты, CI/CD и решение типичных проблем.

Введение

Next.js — один из самых популярных React-фреймворков для создания веб-приложений с серверным рендерингом (SSR), статической генерацией (SSG) и API-роутами. Когда приложение готово к продакшену, встаёт вопрос деплоя. Vercel — отличный выбор, но многие проекты требуют собственного VPS-сервера.

В этом руководстве мы разберём полный цикл деплоя Next.js приложения на VPS с Ubuntu, используя Docker, docker-compose и nginx в качестве reverse proxy. Также настроим SSL-сертификаты через Let’s Encrypt.

Предварительные требования

  • VPS с Ubuntu 22.04 LTS или новее (рекомендуется минимум 1 ГБ RAM, 1 vCPU)
  • Доменное имя, направленное на IP-адрес сервера (A-запись)
  • Права root или пользователь с sudo
  • Docker и Docker Compose установлены на сервере

Установка Docker на Ubuntu:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable docker --now

Проверьте установку:

docker --version
docker compose version

Шаг 1: Dockerfile для Next.js

Создайте Dockerfile в корне вашего Next.js проекта. Мы используем multi-stage build — это уменьшает размер финального образа.

# Стадия сборки
FROM node:20-alpine AS builder
WORKDIR /app

# Копируем файлы зависимостей
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./

# Устанавливаем зависимости
RUN npm ci

# Копируем исходный код
COPY . .

# Собираем приложение
RUN npm run build

# Продакшен-стадия
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Создаём непривилегированного пользователя
RUN addgroup --system --gid 1001 nodejs && \
    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

CMD ["node", "server.js"]

Важное замечание о standalone-режиме

Чтобы standalone сборка работала, добавьте в next.config.js:

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

module.exports = nextConfig;

Режим standalone создаёт автономную сборку, включающую только необходимые файлы — идеально для Docker-контейнеров.

Шаг 2: docker-compose.yml

Файл docker-compose.yml описывает конфигурацию контейнеров. Разместите его в корне проекта:

version: '3.8'

services:
  nextjs:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nextjs-app
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
      - JWT_SECRET=${JWT_SECRET}
    env_file:
      - .env.production
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Шаг 3: Переменные окружения

Создайте .env.production файл (не коммитьте его в Git!):

DATABASE_URL=postgresql://user:password@host:5432/mydb
NEXT_PUBLIC_API_URL=https://api.example.com
JWT_SECRET=ваш-супер-секретный-ключ
NEXTAUTH_SECRET=ваш-nextauth-секрет
NEXTAUTH_URL=https://example.com

Добавьте .env.production в .gitignore:

.env*.production
.env*.local

Шаг 4: Nginx reverse proxy

Установите nginx на сервере:

sudo apt install -y nginx

Создайте конфигурацию сайта:

# /etc/nginx/sites-available/example.com

server {
    listen 80;
    server_name example.com www.example.com;

    # Для Certbot-валидации
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;

        # Таймауты для долгих запросов
        proxy_read_timeout 90s;
        proxy_connect_timeout 90s;
        proxy_send_timeout 90s;
    }
}

Включите конфигурацию:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo mkdir -p /var/www/certbot
sudo nginx -t && sudo systemctl reload nginx

Шаг 5: SSL-сертификаты через Let’s Encrypt

Установите Certbot и плагин для nginx:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Получите сертификат:

sudo certbot --nginx -d example.com -d www.example.com

Certbot автоматически обновит конфигурацию nginx и настроит HTTPS. Сертификаты обновляются автоматически — проверьте таймер:

sudo systemctl status snap.certbot.renew.timer

После настройки SSL итоговая конфигурация nginx будет выглядеть так:

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Шаг 6: Деплой на сервер

Ручной деплой

Скопируйте файлы на сервер:

rsync -avz --exclude 'node_modules' --exclude '.next' --exclude '.git' \
  ./ root@ваш-сервер:/opt/nextjs-app/

На сервере выполните:

cd /opt/nextjs-app
docker compose up -d --build

Проверьте логи:

docker compose logs -f

Makefile для удобства

Создайте Makefile в корне проекта:

.PHONY: deploy logs restart

HOST := example.com
APP_DIR := /opt/nextjs-app

deploy:
	rsync -avz --exclude 'node_modules' --exclude '.next' --exclude '.git' \
		./ root@$(HOST):$(APP_DIR)/
	ssh root@$(HOST) "cd $(APP_DIR) && docker compose up -d --build"

logs:
	ssh root@$(HOST) "cd $(APP_DIR) && docker compose logs -f"

restart:
	ssh root@$(HOST) "cd $(APP_DIR) && docker compose restart"

Теперь деплой одной командой:

make deploy

Шаг 7: CI/CD (краткие советы)

GitHub Actions

Создайте .github/workflows/deploy.yml:

name: Deploy to VPS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /opt/nextjs-app
            git pull origin main
            echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" > .env.production
            docker compose up -d --build

Не забудьте добавить секреты (VPS_HOST, VPS_USER, VPS_SSH_KEY, DATABASE_URL и т.д.) в настройках репозитория GitHub → Settings → Secrets and variables → Actions.

Альтернативно, можно хранить .env.production прямо на сервере (не в репозитории) и управлять переменными вручную. Это проще для небольших проектов, где нет сложного CI/CD.

Типичные проблемы и их решение

1. Ошибка Module not found в standalone-сборке

Если определённые модули не попадают в standalone-сборку, используйте outputFileTracingIncludes в next.config.js:

const nextConfig = {
  output: 'standalone',
  experimental: {
    outputFileTracingIncludes: {
      '/**': ['./prisma/**/*', './public/**/*'],
    },
  },
};

2. Не работают API-роуты после деплоя

Убедитесь, что nginx передаёт заголовки корректно, особенно X-Forwarded-Proto и Host. Проверьте, что в .env.production указан правильный NEXTAUTH_URLhttps://).

3. Контейнер падает с ошибкой памяти

Docker по умолчанию не ограничивает память. Добавьте лимиты в docker-compose.yml:

services:
  nextjs:
    # ... остальная конфигурация
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

4. Картинки не отображаются

Если вы используете next/image с внешними доменами, добавьте их в next.config.js:

const nextConfig = {
  images: {
    domains: ['cdn.example.com', 'images.example.com'],
  },
};

5. Контейнер перезагружается в цикле

Проверьте, что порт 3000 не занят другим процессом:

sudo lsof -i :3000

Или запустите контейнер вручную, чтобы увидеть ошибку:

docker compose up --build  # без -d, чтобы видеть логи в реальном времени

Обслуживание сервера

Автоматическое обновление Docker-образов

Используйте Watchtower для автоматического обновления контейнеров (опционально):

docker run -d \
  --name watchtower \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower \
  --interval 86400

Очистка старых образов

Чтобы не забивать диск, периодически выполняйте:

docker system prune -af --filter "until=168h"

Можно добавить эту команду в cron:

0 3 * * 0 docker system prune -af --filter "until=168h"

Мониторинг

Для базового мониторинга состояния контейнеров:

docker compose ps
docker stats --no-stream

Полезные ссылки

Заключение

Мы разобрали полный цикл деплоя Next.js приложения на VPS: от Dockerfile и docker-compose до обратного прокси nginx и SSL-сертификатов. Такой подход даёт полный контроль над инфраструктурой, позволяет гибко настраивать ресурсы и подходит для проектов любого масштаба.

Ключевые моменты, которые стоит запомнить:

  • Используйте output: 'standalone' в Next.js — это даёт минимальный Docker-образ
  • Храните секреты в .env.production и никогда не коммитьте их
  • Nginx — тонкая прослойка между миром и вашим приложением; уделите внимание настройке заголовков
  • Certbot с автообновлением избавляет от забот о сертификатах
  • CI/CD экономит время даже на небольших проектах — начните с простого GitHub Actions