Деплой Next.js на VPS без Vercel и без Docker сводится к трём вещам: собрать проект в режиме standalone, запустить получившийся server.js как службу systemd и поставить перед ним Nginx с HTTPS. В итоге приложение работает от отдельного пользователя без прав, само поднимается после сбоя и перезагрузки, а статические файлы отдаёт Nginx, не нагружая Node.js. Ниже — все шаги с проверкой, замеры памяти и короткий вариант с Docker.

Проверено на Ubuntu 24.04 и 26.04 с Next.js 16.4.0, Node.js 24.21.0 и Nginx из репозиториев Ubuntu (1.24 и 1.28). Шаг с HTTPS требует настоящего домена, поэтому в тестовой среде проверена только установка certbot, а команды получения сертификата взяты из документации certbot. Вместо 203.0.113.10 подставьте адрес своего сервера, вместо example.com — свой домен.

Что нужно заранее

  • Сервер с Ubuntu 24.04 или 26.04, пользователь с sudo и открытые порты 80 и 443 — см. первую настройку сервера.
  • Домен, у которого A-запись указывает на адрес сервера (нужен для HTTPS).
  • Проект на Next.js. Для проверки мы взяли стартовый: npx create-next-app@latest myapp --yes.

Сколько ресурсов нужно

Замеры на стартовом проекте Next.js 16.4.0:

Что Память
Запущенный server.js сразу после старта около 60 МБ
Он же после 2 000 запросов страницы около 170 МБ
Сборка npm run build (все процессы вместе) до 0,9 ГБ

Работающему приложению хватает сервера с 1 ГБ памяти, если на нём нет тяжёлой базы данных. Сборка требует заметно больше: в тестовой среде с нехваткой памяти она трижды оборвалась с сообщением Killed. Поэтому на сервере с 1–2 ГБ либо добавьте файл подкачки (как — в статье о первой настройке), либо собирайте проект в GitHub Actions или на своём компьютере и загружайте готовую сборку. У настоящего проекта с десятками страниц и зависимостей цифры будут выше — замерьте свой.

Шаг 1. Установить Node.js 24 LTS

Next.js 16 требует Node.js не ниже 20.9. Node.js 20 перестал получать обновления в апреле 2026 года, поэтому ставьте текущую LTS-версию — 24 (обновления до апреля 2028 года). В репозитории Ubuntu 24.04 лежит Node.js 18 — для Next.js 16 он уже не подходит; в Ubuntu 26.04 — Node.js 22, он работает, но поддерживается только до апреля 2027 года. Поставим Node.js 24 из репозитория NodeSource:

curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install -y nodejs
node -v

Последняя команда должна вывести v24.… (у нас — v24.21.0). Дальше Node.js обновляется вместе с системой через apt.

Шаг 2. Собрать проект в режиме standalone

В обычном режиме для запуска next start на сервере нужен весь node_modules. В режиме standalone сборка сама отбирает только нужные файлы и создаёт готовый к запуску server.js. Включите его в next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

Если в файле уже есть другие настройки, просто добавьте строку output: "standalone",. Соберите проект и докопируйте статические файлы — по документации Next.js сборка их в standalone не кладёт:

npm ci
npm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/

Проверка: в .next/standalone лежат server.js, package.json, node_modules, public и .next с каталогом static внутри. У стартового проекта всё вместе занимает около 40 МБ.

Две особенности сборки:

  • Переменные NEXT_PUBLIC_… подставляются в код при сборке. Если они нужны в браузере, задайте их до npm run build, а не в файле окружения на сервере. Остальные переменные читаются при запуске.
  • Сборка привязана к платформе. В node_modules попадает модуль обработки картинок sharp под систему и процессор сборщика. Собирайте на Linux той же архитектуры, что и сервер (x86-64 или ARM): сборка с macOS или Windows на Linux-сервере не сможет обрабатывать изображения. Раннер GitHub ubuntu-24.04 подходит для обычного x86-64 сервера.

Шаг 3. Пользователь и каталог на сервере

Приложение будет работать от системного пользователя nextjs без пароля и без входа в систему:

sudo useradd --system --home-dir /srv/nextjs --shell /usr/sbin/nologin nextjs
sudo install -d -o nextjs -g nextjs -m 755 /srv/nextjs /srv/nextjs/app

Загрузите сборку. Если собирали на своём компьютере или в CI — сначала во временный каталог на сервере (admin — ваш пользователь с sudo):

rsync -az --delete .next/standalone/ admin@203.0.113.10:nextjs-build/

Затем на сервере переложите её на место, сделав владельцем nextjs:

sudo rsync -a --delete --chown=nextjs:nextjs ~/nextjs-build/ /srv/nextjs/app/

Если собирали прямо на сервере, достаточно второй команды с путём к .next/standalone/ вместо ~/nextjs-build/. Если rsync не найден, поставьте его: sudo apt install -y rsync.

Шаг 4. Переменные окружения

Секреты — адрес базы данных, ключи API — храните не в репозитории, а в файле, который может прочитать только служба:

sudo install -d -m 750 -o root -g nextjs /etc/nextjs
sudo tee /etc/nextjs/env > /dev/null <<'EOF'
# Секреты и настройки приложения, читаются при запуске
DATABASE_URL=postgres://app:secret@127.0.0.1:5432/app
EOF
sudo chown root:nextjs /etc/nextjs/env
sudo chmod 640 /etc/nextjs/env

Владелец файла — root, группа nextjs может только читать, остальные пользователи сервера его не видят. Если база данных ещё не установлена, см. PostgreSQL на Ubuntu.

Шаг 5. Служба systemd для Next.js

sudo tee /etc/systemd/system/nextjs.service > /dev/null <<'EOF'
[Unit]
Description=Next.js
After=network.target

[Service]
Type=simple
User=nextjs
Group=nextjs
WorkingDirectory=/srv/nextjs/app
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=HOSTNAME=127.0.0.1
EnvironmentFile=/etc/nextjs/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nextjs

Что здесь важно:

  • HOSTNAME=127.0.0.1 — server.js слушает только локальный адрес, и снаружи к нему можно попасть лишь через Nginx. Без этой строки сервер слушает все адреса, и порт 3000 будет открыт в интернет, если его не закрыл файрвол.
  • PORT=3000 — порт, на который Nginx будет передавать запросы.
  • Restart=on-failure перезапускает приложение через 5 секунд после аварийного завершения, enable — запускает его после перезагрузки сервера.
  • NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp — встроенная в systemd защита: процесс не может получить больше прав, не может писать в системные каталоги и не видит домашние каталоги пользователей.

Проверка:

systemctl status nextjs --no-pager
ss -ltnp | grep 3000
curl -sI http://127.0.0.1:3000/ | head -1

Должно быть Active: active (running), адрес 127.0.0.1:3000 и HTTP/1.1 200 OK. Журнал приложения — journalctl -u nextjs -f.

Шаг 6. Nginx как обратный прокси

Nginx принимает запросы на портах 80 и 443, передаёт их приложению и сам отдаёт файлы из /_next/static/:

sudo apt install -y nginx
sudo tee /etc/nginx/sites-available/nextjs > /dev/null <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Файлы сборки: имена содержат хеш, поэтому их можно кешировать навсегда
    location /_next/static/ {
        alias /srv/nextjs/app/.next/static/;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    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;
    }
}
EOF
sudo ln -s /etc/nginx/sites-available/nextjs /etc/nginx/sites-enabled/nextjs
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
  • proxy_set_header передают приложению настоящий домен, адрес посетителя и протокол — без них Next.js видел бы все запросы как пришедшие с 127.0.0.1 по HTTP.
  • /_next/static/ — JS, CSS и шрифты сборки. В их именах есть хеш содержимого, поэтому браузеру можно хранить их год (immutable): после новой сборки имена изменятся. Nginx отдаёт эти файлы с диска быстрее, чем Node.js.

Проверка — с любого компьютера:

curl -sI http://example.com/ | head -1

Ответ HTTP/1.1 200 OK. Откройте сайт в браузере, в инструментах разработчика найдите любой файл /_next/static/…: у него заголовки Server: nginx и Cache-Control: public, max-age=31536000, immutable.

Шаг 7. HTTPS с Let's Encrypt

Бесплатный сертификат Let's Encrypt выпускает и продлевает программа certbot. Она сама допишет в настройки Nginx блок для порта 443 и переадресацию с HTTP на HTTPS. Домен уже должен указывать на сервер, порты 80 и 443 — быть открыты.

Установка по инструкции certbot:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --nginx -d example.com -d www.example.com

Certbot спросит почту для уведомлений, попросит принять условия и выпустит сертификат. Если у домена нет www, уберите второй -d. Вместо snap можно поставить пакет из репозитория Ubuntu: sudo apt install -y certbot python3-certbot-nginx (certbot 2.9 в Ubuntu 24.04, 4.0 в 26.04) — он тоже содержит модуль для Nginx и сам включает таймер продления certbot.timer.

Проверка: curl -sI https://example.com/ | head -1 отвечает HTTP/1.1 200 OK, а пробное продление проходит без ошибок:

sudo certbot renew --dry-run

Сертификат действует 90 дней, certbot продлевает его автоматически. Расписание видно в systemctl list-timers | grep certbot.

Как обновлять приложение

Новая версия — те же шаги: собрать, докопировать public и .next/static, загрузить и перезапустить службу:

sudo rsync -a --delete --chown=nextjs:nextjs ~/nextjs-build/ /srv/nextjs/app/
sudo systemctl restart nextjs

На время перезапуска — в нашей проверке около секунды — Nginx отвечает 502. Чтобы обновлять сайт автоматически при push в main, проверять его после выкладки и откатываться за секунду, используйте схему из статьи «Деплой на VPS из GitHub Actions по SSH»: в ней релизы лежат в отдельных каталогах, а служба смотрит на ссылку current.

Вариант с Docker

Если на сервере уже есть Docker, ту же сборку можно упаковать в образ. Dockerfile в корне проекта:

FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Рядом положите .dockerignore со строками node_modules, .next и .git. Сборка и запуск:

docker build -t myapp .
docker run -d --name myapp --restart unless-stopped \
  -p 127.0.0.1:3000:3000 --env-file /etc/nextjs/env myapp

Внутри контейнера приложение слушает все адреса (HOSTNAME=0.0.0.0), иначе Docker не сможет передать ему запросы, а наружу порт опубликован только на 127.0.0.1. Настройки Nginx и certbot те же, только блок location /_next/static/ уберите: файлов на диске сервера нет, и статику отдаёт сам Next.js — с тем же заголовком immutable. Образ стартового проекта занимает около 200 МБ, контейнер — около 100–150 МБ памяти.

Если не работает

Что видно Что делать
502 Bad Gateway от Nginx Приложение не запущено или упало: systemctl status nextjs и journalctl -u nextjs -n 50. Проверьте, что порт в proxy_pass совпадает с PORT
Страница без стилей, файлы /_next/static/… отвечают 404 Не скопирован .next/static в .next/standalone/.next/ перед загрузкой. Повторите шаг 2
Картинки через next/image не показываются Сборка сделана на другой системе или процессоре, и sharp не подходит серверу. Собирайте на Linux той же архитектуры
Сайт открывается по http://203.0.113.10:3000 Не задан HOSTNAME=127.0.0.1 в службе, и порт открыт в файрволе
В браузере переменная NEXT_PUBLIC_… пустая Её не было при сборке. Задайте её до npm run build и пересоберите
Сборка обрывается с сообщением Killed Не хватило памяти. Добавьте файл подкачки или собирайте в CI и загружайте готовую сборку
certbot не может подтвердить домен A-запись домена не указывает на сервер или закрыт порт 80. Проверьте на своём компьютере dig +short example.com, на сервере — sudo ufw status

Что дальше

  • Деплой на VPS из GitHub Actions по SSH — автоматическая выкладка с проверкой и откатом.
  • Первая настройка сервера на Ubuntu — файрвол, обновления безопасности и подкачка до запуска сайта.
  • PostgreSQL на Ubuntu — база данных для приложения на том же сервере.
  • Сборка Next.js занимает до 1 ГБ памяти — как выбрать размер сервера, в статье «Сколько оперативной памяти нужно VPS».
  • Несколько сайтов на одном сервере и продление сертификатов (Let's Encrypt переходит на 64-дневные сертификаты) — Nginx reverse proxy и certbot.
  • Способы установить Node.js и перейти на новую версию — как установить Node.js на сервер.