Введение
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.comCertbot автоматически обновит конфигурацию 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 -fMakefile для удобства
Создайте 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_URL (с https://).
3. Контейнер падает с ошибкой памяти
Docker по умолчанию не ограничивает память. Добавьте лимиты в docker-compose.yml:
services:
nextjs:
# ... остальная конфигурация
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M4. Картинки не отображаются
Если вы используете 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
- Next.js Deployment Guide
- Docker Documentation
- Docker Compose Documentation
- Let’s Encrypt (Certbot)
- Nginx Documentation
Заключение
Мы разобрали полный цикл деплоя Next.js приложения на VPS: от Dockerfile и docker-compose до обратного прокси nginx и SSL-сертификатов. Такой подход даёт полный контроль над инфраструктурой, позволяет гибко настраивать ресурсы и подходит для проектов любого масштаба.
Ключевые моменты, которые стоит запомнить:
- Используйте
output: 'standalone'в Next.js — это даёт минимальный Docker-образ - Храните секреты в
.env.productionи никогда не коммитьте их - Nginx — тонкая прослойка между миром и вашим приложением; уделите внимание настройке заголовков
- Certbot с автообновлением избавляет от забот о сертификатах
- CI/CD экономит время даже на небольших проектах — начните с простого GitHub Actions