Деплой 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-сервере не сможет обрабатывать изображения. Раннер GitHububuntu-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 на сервер.