Макар Кузьмичев
Назад

Как Макар свой блог деплоил

История о том, как я деплоил на vps-сервер блог на next.js

🚀 Деплой блога на Next.js на VPS с Docker и GitHub Actions

В этой статье я расскажу, как я задеплоил свой блог на Next.js на VPS-сервер. В процессе я попробовал разные подходы: от запуска через pm2, ручного копирования билдов — до полноценного CI/CD через Docker и GitHub Actions. Делюсь пошагово своими выводами, ошибками и опытом.


🧱 1. Разработка блога на Next.js

Я создал личный блог на Next.js — фреймворке, который отлично подходит как для статических сайтов, так и SSR-приложений. По крайней мере, так говорит документация и маркетологи. В проекте реализованы:

  • маршрутизация страниц,
  • генерация статей из Markdown-файлов,
  • адаптивная вёрстка.

Когда всё было готово — пришло время деплоить.


☁ 2. Публикация на VPS

У меня был VPS-сервер, и я решил выложить блог туда. Попробовал несколько подходов, прежде чем нашёл оптимальный.


🔧 Шаг 1: Настройка VPS-сервера

Для начала необходимо установить веб-сервер. Пробовал Apache и Nginx. Nginx показался гораздо удобнее с точки зрения конфигурации, поэтому выбрал его.

Итак, вот мы зашли на наш арендованный сервер.

  1. Устанавливаем Nginx:
sudo apt update
sudo apt install nginx
  1. Далее создаём файл конфигурации для сайта:
sudo nano /etc/nginx/sites-available/mallky.ru
  1. Собственно, прописываем конфиги:
server {
    listen 80;
    server_name mallky.ru www.mallky.ru;

    root /var/www/mallky.ru;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

В данном конфиге слушается только 80 порт, что соответствует http. Я же хотел https (я же не какой-то хер с горы), значит, нужно настроить редирект с 80 порта на 443:

server {
    listen 80;
    server_name mallky.ru;
    # Вот он, редирект
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name mallky.ru;

    root /var/www/html/mallky.ru;
    index index.html;
}
  1. А дальше нужны сертификаты SSL. Возникает вопрос — кому нужны?


    Краткое отступление с красивыми иконками от ChatGPT:

  2. SSL-сертификат обеспечивает безопасное соединение между пользователем и сервером. Он:

  • 🔐 Шифрует данные, чтобы их нельзя было перехватить (например, логины, пароли, платёжные данные).

  • ✅ Делает сайт более доверенным: браузер показывает замочек 🔒.

  • 🌐 Позволяет использовать HTTPS вместо HTTP.

  • 📈 Влияет на SEO (ранжирование в Google).

Шифровать мне особо нечего (пока), а вот в поисковиках хочется быть на первых местах.

  1. Как работает (упрощённо):
  • Браузер подключается к сайту по HTTPS.

  • Сервер отправляет SSL-сертификат.

  • Браузер проверяет, доверяет ли этому сертификату (подписан ли авторитетным удостоверяющим центром).

  • Если всё ок, устанавливается зашифрованное соединение (TLS-сессия).

Конец краткого отступления


Получается, что мне нужны сертификаты. Значит, генерим сертификаты, желательно бесплатно, без смс и регистрации, то есть с помощью Let's Encrypt.

  • Устанавливаем Certbot (если не установлен):
sudo apt update
sudo apt install certbot python3-certbot-nginx
  • Генерируем и устанавливаем SSL-сертификат:
sudo certbot --nginx -d mallky.ru -d www.mallky.ru

Certbot за тебя проверит доступность домена, получит сертификаты от Let's Encrypt, настроит Nginx автоматически, перезапустит Nginx, в общем, прямо сделает всё красиво.

Главное помнить, что сертификат Let's Encrypt действует 90 дней, но его можно автоматически продлевать.

sudo certbot renew --dry-run

И тут ты, конечно, задашь вопрос: "А где же эти сертификаты теперь найти? Как их дописать в конфиг nginx'a?"

Обычно ты их сможешь найти вот тут:

# Сертификат
/etc/letsencrypt/live/mallky.ru/fullchain.pem
# Ключ
/etc/letsencrypt/live/mallky.ru/privkey.pem
  1. Допишем SSL-сертификаты в конфиг сервера:
  server {
    listen 80;
    server_name mallky.ru;
    # Вот он, редирект
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name mallky.ru;

    root /var/www/html/mallky.ru;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/mallky.ru/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mallky.ru/privkey.pem; # managed by Certbot
  }
  1. Активируем сайт:
sudo ln -s /etc/nginx/sites-available/mallky.ru /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Шаг 2: Деплоим сайт на сервер

Способ 1: В лоб

Сначала я решил выложить весь проект как есть на сервер и запускать его с помощью pm2. План был такой:

  1. Подключаюсь по SSH
  2. Клонирую репозиторий
  3. Делаю npm install
  4. А потом резко npm run build и pm2 start npm -- start

Но на практике всё оказалось не так просто (неожиданно, да).

⚠ Проблема:

У моего VPS было мало оперативной памяти (512–1024 МБ). А всё потому, что для меня этот VPS-сервер — игрушка, я хочу вспомнить, как выкладывал сайтики раньше, поэтому взял что попроще да подешевле.

Так вот, на этапе npm install сервер зависал и убивал процесс по OOM (Out Of Memory), свободной оперативной памяти на тот момент было 300 MB. Казалось бы, вполне достаточно, но, видимо, next.js имеет огромное количество зависимостей, или npm так устроен, что 300 MB оказалось мало. В чём конкретно проблема, выяснять не стал. Если знаешь — поделись в телеграме, мне будет очень интересно.

До npm run build на сервере, как ты понимаешь, я даже не дошёл.
ШТОШ, план изначально попахивал чем-то нехорошим, проще говоря, был дерьмовенький.

Итого

➖ Минусы подхода:
  • Сильно зависит от ресурсов сервера
  • Занимает много времени
  • Как минимум нестабильно на слабом VPS
➕ Плюсы:
  • Простой и понятный подход, особенно если ты любишь всё делать в лоб
  • Удобно дебажить прямо на сервере

Способ 2: Билд локально, деплой билда на сервер

Я решил попробовать собрать проект локально и загружать только результат сборки на сервер.

Как это должно работать:

  1. Подготавливаем next.config.js:
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',
};

export default nextConfig;
  1. Локально жмякаем npm install && npm run build && next export
  2. После этого в папке out найдёшь полностью статическую версию сайта (HTML, CSS, JS).
  3. Загружаем всю папку на сервер, радуемся, как всё работает
➕ Плюсы:
  • Снижает нагрузку на сервер
  • Быстрый деплой
  • Вспоминаем, какой был веб на заре своего существования
  • Легко автоматизировать (хотя бы простым bash-скриптом)
➖ Минусы:
  • Нужно следить за правильной структурой файлов
  • Нужно соблюдать некоторые правила внутри next.js. Например, нельзя использовать getServerSideProps — только getStaticProps
  • SSR фактически отсутствует, так как сразу генерим всю статику
  • Мы всё-таки в 2025

Поэтому

Способ 3: Docker + GitHub Actions

В итоге я пришёл к такому варианту — Docker + CI/CD (так-то пока только CD) через GitHub Actions.

Как я пришёл к такому варианту... Ну, во-первых, docker-контейнер — это круто, своя изолированная система и т.д. и т.п., везде приложение работает одинаково, чудо право-слово. Во-вторых, современный веб же. В-третьих, придумай сам/сама.

Что сделал:

  1. Написал Dockerfile, конечно. Ну как написал... Стянул из примера в официальной документации next.js'а:
# syntax=docker.io/docker/dockerfile:1

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN \
  if [ -f yarn.lock ]; then yarn run build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder /app/articles ./articles

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
  1. Добавил .github/workflows/deploy.yml для GitHub Actions:
name: Deploy to VPS

on:
  push:
    branches: ['main']

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

      - name: Build Docker image
        run: docker build --platform linux/amd64 -t my-blog .

      - name: Save image to file
        run: docker save my-blog > my-blog.tar

      - name: Deploy to VPS
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.SSH_KEY }}
          source: 'my-blog.tar'
          target: '/var/www/html/mallky.ru/'

      - name: Run on VPS
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            docker stop my-blog || true
            docker rm my-blog || true
            docker load < /var/www/html/mallky.ru/my-blog.tar
            docker run -d --name my-blog -p 3000:3000 my-blog:latest
  1. Не забыл поправить next.config.ts:
import type { NextConfig } from 'next';

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

export default nextConfig;
➕ Плюсы:
  • Автоматизация: пуш → деплой
  • Контроль: код билдится в изолированной среде
  • Удобство: не нужно ничего делать вручную
  • Повторяемость: одно и то же окружение везде
➖ Минусы:
  • Много места съедает docker с образами на сервере. А там места не густо, всего 10 GB

Чему же я научился

Возникает вопрос — и надо оно было тебе? Можно же было взять обычный хостинг, было бы дешевле и, возможно, проще. Ответ: да, мне было весело. Ведь я:

  1. Потрогал ubuntu на своём VPS-сервере. Попробовал в настройку сервера через командную строку.
  2. Поигрался с безопасностью: SSH-ключи, ufw, fail2ban
  3. Почитал про Nginx и его настройку: HTTPS, редиректы, прокси
  4. Трогал настройки для Next.js (SSR, статика)
  5. Поработал с Docker (давно пора было!)
  6. CD через GitHub Actions:
    • Настройка SSH-доступа через Secrets
    • CI/CD пайплайн
    • Деплой через push в main

Вместо заключения

Путь от «простого Next.js проекта» до автодеплоя через GitHub Actions был не самый короткий, но зато очень полезный и познавательный. Стало ясно, что:

  • уметь эффективно работать с сервером очень важно
  • современные инструменты жрут офигеть как много памяти (любой)
  • использовать Docker и GitHub Actions в реальных проектах удобно и приятно.
Больше всякого можно найти тут:Канал в telegram