Перейти к содержимому

Деплой Next.js с GitLab CI/CD: От настройки сервера до автоматизации

Константин Потапов
30 мин

Полный гайд по настройке автоматического деплоя Next.js приложения на собственный сервер через GitLab CI/CD. PM2 для zero-downtime, Nginx Proxy Manager для управления доменами, secrets management и multi-environment setup.

Деплой Next.js с GitLab CI/CD: От настройки сервера до автоматизации

Коротко

Vercel и Netlify в России то открываются, то нет. Своего Vercel нет.

Связка: GitLab CI, свой сервер, PM2 cluster, Nginx Proxy Manager. Push в main, сайт живой, SSL сам. Один IP, несколько доменов, счёт предсказуемый.

GitLab CI git pull npm ci npm run build PM2 reload готово

Для продакшена ещё бэкап, health check, rollback, staging. Если не хочется крутить руками: Coolify, Dokploy, CapRover.

Два часа на минималку. Полдня на полный стек.

Почему свой сервер

potapov.me крутится так. Не из любви к systemd. Из того, что площадка не должна уехать в бан в пятницу. VPS стоит одну сумму. Pay-per-use прилетает счётом после всплеска. Свой Node, свои зависимости, своя сеть.

Цена: время на настройку, свои апдейты, свой firewall.

Ниже полный стек. На личном сайте живёт минималка: pull, ci, build, PM2 reload. Остальное берите как чеклист, не как религию.

Архитектура решения

Компоненты системы

┌─────────────┐      ┌──────────────┐      ┌──────────────────────┐
│   GitLab    │─────▶│ GitLab Runner│─────▶│   Server Cluster     │
│ (Git + CI)  │      │   (Build)    │      │                      │
└─────────────┘      └──────────────┘      │ ┌────────────────┐   │
      │                                    │ │ Nginx Proxy    │   │
      │ 1. Push code                       │ │ Manager (NPM)  │   │
      │ 2. Trigger pipeline                │ └────────┬───────┘   │
      │ 3. Build & test                    │          │           │
      │ 4. Deploy via SSH ─────────────────┼──────────┤           │
      │ 5. Health check                    │ ┌────────▼───────┐   │
      │ 6. Rollback if needed              │ │  Next.js App   │   │
      │                                    │ │  (PM2 Cluster) │   │
      │                                    │ └────────────────┘   │
      └────────────────────────────────────┴──────────────────────┘
                                              1 External IP
                                              Multiple Apps/Servers

Почему именно эта связка?

  1. GitLab CI: встроенный в GitLab, бесплатный для self-hosted, мощный YAML DSL
  2. PM2: production process manager с cluster mode для zero-downtime reload
  3. Nginx Proxy Manager: удобный web-интерфейс для управления reverse proxy, SSL сертификатами и множественными доменами на одном IP
  4. Ubuntu 24.04: LTS релиз с долгосрочной поддержкой

Почему Nginx Proxy Manager, а не просто Nginx?

Если у вас один внешний IP-адрес и несколько серверов/приложений (как в моем случае: целый кластер), то управлять через конфиги Nginx становится неудобно. Nginx Proxy Manager (NPM) решает это:

  • Web-интерфейс для управления доменами и прокси
  • Автоматические SSL сертификаты через Let's Encrypt
  • Легкое добавление новых доменов без редактирования конфигов
  • Поддержка proxy hosts, streams, редиректов
  • Access lists и защита от ботов из коробки

Для простых случаев (1 сервер, 1 домен) можно использовать обычный Nginx: принципы те же.

Workflow деплоя

  1. Developer пушит код в ветку main или develop
  2. GitLab CI запускает pipeline:
    • Install: установка зависимостей с кэшированием
    • Lint: проверка кода (ESLint, TypeScript)
    • Test: запуск тестов (опционально)
    • Build: production сборка Next.js
    • Deploy: деплой на сервер через SSH
  3. Server получает новый код и:
    • Бэкапит текущую версию
    • Устанавливает зависимости
    • Собирает приложение (или копирует артефакт)
    • Выполняет pm2 reload для zero-downtime перезапуска
    • Проверяет health endpoint
  4. Rollback автоматически выполняется при ошибках

Сервер

Требования к железу

Для комфортной работы Next.js приложения рекомендую:

  • CPU: 2 vCPU (минимум 1 vCPU, но сборка будет медленной)
  • RAM: 2 GB (минимум 1 GB + 2 GB swap для сборки)
  • Диск: 20 GB SSD (10 GB минимум для кода, node_modules, .next, логов)
  • ОС: Ubuntu 24.04 LTS
  • Сеть:
    • Входящие: 22 (SSH), 80 (HTTP), 443 (HTTPS)
    • Исходящие: 443 (npm, git, certbot)

Важно: Если у вас меньше 2 GB RAM, обязательно настройте swap минимум 2 GB. Сборка Next.js с Turbopack требовательна к памяти.

Шаг 1: Базовая настройка сервера и безопасность

Подключаемся к серверу по SSH и обновляем систему:

# Обновление системы
sudo apt update && sudo apt upgrade -y
 
# Установка базовых пакетов
sudo apt install -y git curl ufw fail2ban build-essential
 
# Настройка firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

SSH Hardening (укрепление безопасности)

Важно для production: Эти настройки защитят ваш сервер от несанкционированного доступа. Обязательно настройте перед тем, как открывать сервер в интернет.

# 1. Создаем SSH ключ на локальной машине (если еще нет)
# Выполняйте на ЛОКАЛЬНОЙ машине, не на сервере!
ssh-keygen -t ed25519 -C "your_email@example.com"
 
# 2. Копируем публичный ключ на сервер
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your_server_ip
 
# 3. Проверяем, что можем зайти по ключу
ssh user@your_server_ip
 
# 4. Отключаем вход по паролю (только SSH ключи)
sudo nano /etc/ssh/sshd_config
 
# Найдите и измените эти строки:
# PasswordAuthentication no
# PubkeyAuthentication yes
# PermitRootLogin no
# ChallengeResponseAuthentication no
 
# 5. Перезапускаем SSH
sudo systemctl restart sshd
 
# 6. НЕ ЗАКРЫВАЙТЕ текущую сессию! Откройте новое окно терминала
# и проверьте, что можете зайти по ключу

Fail2Ban для защиты от брутфорса

# Установка (уже установлен выше)
sudo apt install -y fail2ban
 
# Создаем локальную конфигурацию
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
 
# Настраиваем базовую защиту SSH
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
destemail = your_email@example.com
sendername = Fail2Ban
 
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
EOF
 
# Запускаем Fail2Ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
 
# Проверяем статус
sudo fail2ban-client status
sudo fail2ban-client status sshd

Что делает Fail2Ban:

  • Анализирует логи SSH
  • При 5 неудачных попытках входа за 10 минут банит IP на 1 час
  • Защищает от брутфорс-атак

Настройка Firewall (UFW)

Более детальная конфигурация портов:

# Сбрасываем правила (если нужно начать с чистого листа)
sudo ufw --force reset
 
# Политика по умолчанию: запретить все входящие, разрешить исходящие
sudo ufw default deny incoming
sudo ufw default allow outgoing
 
# Разрешаем SSH (ВАЖНО: сделайте это до enable!)
sudo ufw allow 22/tcp comment 'SSH'
 
# Разрешаем HTTP/HTTPS для NPM или Nginx
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
 
# Если используете NPM, разрешаем админку (только для вашего IP!)
# sudo ufw allow from YOUR_IP to any port 81 comment 'NPM Admin'
 
# Ограничение скорости подключений SSH (защита от DDoS)
sudo ufw limit 22/tcp
 
# Включаем firewall
sudo ufw enable
 
# Проверяем правила
sudo ufw status numbered

Осторожно! Перед ufw enable убедитесь, что разрешили порт SSH (22), иначе потеряете доступ к серверу!

Шаг 2: Установка Node.js 20 LTS

Next.js 15 требует Node.js >= 18.17, рекомендуется 20 LTS:

# Установка Node.js 20 через NodeSource
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
 
# Проверка версий
node -v  # должно быть >= v20.x
npm -v   # должно быть >= 10.x
 
# Настройка npm для production
npm config set loglevel error

Альтернатива: Можно использовать nvm (Node Version Manager) для управления версиями Node.js, особенно если нужно несколько версий на одном сервере.

Шаг 3: Установка и настройка PM2

PM2: production process manager с поддержкой cluster mode для zero-downtime deployment.

# Установка PM2 глобально
sudo npm install -g pm2
 
# Настройка автозапуска PM2 при перезагрузке
pm2 startup systemd -u $USER --hp $HOME
# Команда выведет строку с sudo: выполните её
 
# Проверка systemd сервиса
sudo systemctl status pm2-$USER

Почему PM2, а не systemd напрямую?

  • Cluster mode с автоматической балансировкой нагрузки
  • Graceful reload без простоя (плавное переключение между инстансами)
  • Встроенный мониторинг (CPU, RAM, logs)
  • Автоматический рестарт при падении
  • Log rotation из коробки
  • Простой API для управления процессами

Шаг 4: Настройка Nginx Proxy Manager

Nginx Proxy Manager (NPM): это web-интерфейс для управления Nginx как reverse proxy. Особенно удобен, когда у вас один внешний IP и несколько приложений/серверов.

Установка через Docker Compose

# Создаем директорию для NPM
mkdir -p ~/nginx-proxy-manager
cd ~/nginx-proxy-manager
 
# Создаем docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'     # HTTP
      - '443:443'   # HTTPS
      - '81:81'     # Admin UI
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      DB_SQLITE_FILE: "/data/database.sqlite"
EOF
 
# Запускаем NPM
docker-compose up -d
 
# Проверяем статус
docker-compose ps

Первичная настройка

  1. Откройте http://YOUR_SERVER_IP:81 в браузере
  2. Войдите с дефолтными credentials:
    • Email: admin@example.com
    • Password: changeme
  3. Смените email и пароль при первом входе

Добавление Proxy Host для Next.js

В веб-интерфейсе NPM:

  1. Hosts → Proxy Hosts → Add Proxy Host

  2. Details tab:

    • Domain Names: potapov.me, www.potapov.me
    • Scheme: http
    • Forward Hostname / IP: localhost (или IP сервера с Next.js)
    • Forward Port: 3000
    • ✅ Cache Assets
    • ✅ Block Common Exploits
    • ✅ Websockets Support
  3. SSL tab:

    • ✅ SSL Certificate: Request a new SSL Certificate
    • ✅ Force SSL
    • ✅ HTTP/2 Support
    • ✅ HSTS Enabled
    • Email: ваш email для Let's Encrypt
    • ✅ I Agree to the Let's Encrypt Terms of Service
  4. Advanced (опционально):

# Кастомные настройки для Next.js
location /_next/static/ {
    proxy_cache_valid 200 60m;
    add_header Cache-Control "public, immutable";
}
 
location /api/health {
    access_log off;
}
 
# Таймауты для SSR
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
  1. Save

NPM автоматически:

  • Настроит reverse proxy на ваше приложение
  • Получит SSL сертификат от Let's Encrypt
  • Настроит автоматическое обновление сертификатов
  • Включит редирект с HTTP на HTTPS

Преимущества NPM в моем случае:

У меня один внешний IP-адрес и целый кластер серверов с разными приложениями. NPM позволяет:

  • Управлять всеми доменами из одного места
  • Добавлять новые приложения за минуту через UI
  • Не редактировать конфиги вручную
  • Автоматически управлять SSL для всех доменов
  • Настраивать access lists, rate limiting, редиректы

Если у вас простая конфигурация (1 сервер, 1-2 домена), можете использовать обычный Nginx: принципы проксирования те же.

Альтернатива: обычный Nginx

Если предпочитаете классический Nginx, пример конфигурации:

# /etc/nginx/sites-available/potapov.me
server {
    listen 80;
    server_name potapov.me www.potapov.me;
 
    location / {
        proxy_pass http://localhost: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 60s;
    }
}

Затем используйте Certbot для SSL:

sudo certbot --nginx -d potapov.me -d www.potapov.me

Шаг 6: Создание структуры каталогов

# Создаем каталог для приложения
sudo mkdir -p /var/www/potapov.me
sudo chown -R $USER:$USER /var/www/potapov.me
 
# Создаем каталоги для логов и бэкапов
mkdir -p /var/www/potapov.me/{logs,backups}
 
# Создаем .env файл (заполним позже через GitLab CI)
touch /var/www/potapov.me/.env

PM2

PM2 Ecosystem файл: это конфигурация для управления приложением в production.

Ecosystem файл хранится в репозитории и задает параметры запуска: количество инстансов, переменные окружения, пути к логам, лимиты памяти и прочее.

Создаем ecosystem.config.cjs в корне проекта:

// ecosystem.config.cjs
module.exports = {
  apps: [
    {
      name: "potapov-me",
      cwd: "/var/www/potapov.me",
      script: "node_modules/next/dist/bin/next",
      args: "start -p 3000",
 
      // Environment variables
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
 
      // Cluster mode для zero-downtime reload
      exec_mode: "cluster",
      instances: 2, // или 'max' для использования всех CPU ядер
 
      // Auto-restart settings
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      max_memory_restart: "500M", // Рестарт при превышении памяти
 
      // Logging
      out_file: "./logs/pm2-out.log",
      error_file: "./logs/pm2-error.log",
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",
      merge_logs: true,
 
      // Graceful shutdown
      kill_timeout: 5000,
      wait_ready: true,
      listen_timeout: 10000,
    },
  ],
 
  // Deployment configuration (опционально, используем GitLab CI)
  deploy: {
    production: {
      user: "deployer",
      host: "potapov.me",
      ref: "origin/main",
      repo: "git@gitlab.com:username/potapov.me.git",
      path: "/var/www/potapov.me",
      "post-deploy":
        "npm ci && npm run build && pm2 reload ecosystem.config.cjs --env production",
    },
  },
};

Ключевые параметры:

  • exec_mode: 'cluster': запуск нескольких инстансов для балансировки и zero-downtime reload
  • instances: 2: количество процессов (рекомендую 2-4 для типичного VPS)
  • max_memory_restart: автоматический рестарт при утечках памяти
  • wait_ready: ждать сигнала готовности от приложения (требует process.send('ready') в коде)
  • kill_timeout: время на graceful shutdown (завершение текущих запросов)

Pro tip: Для Next.js в production режиме достаточно 2-4 инстансов. Больше не всегда = быстрее, особенно на малоресурсных VPS.

Добавляем health check endpoint

Создаем API route для проверки здоровья приложения:

// app/api/health/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  const health = {
    status: "ok",
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    environment: process.env.NODE_ENV,
  };
 
  return NextResponse.json(health, { status: 200 });
}

Этот endpoint используется для:

  • Проверки доступности после деплоя
  • Мониторинга через внешние сервисы (UptimeRobot, Pingdom)
  • Load balancer health checks

GitLab CI

Теперь настроим автоматический деплой через GitLab CI.

Архитектура CI/CD

GitLab CI использует Runners: агенты, которые выполняют задачи из pipeline. Бывают:

  1. Shared Runners: предоставляются GitLab.com бесплатно (лимит 400 минут/месяц)
  2. Specific Runners: ваш собственный раннер на любом сервере
  3. Group/Project Runners: для группы проектов или конкретного репозитория

Для этого гайда используем Shared Runners GitLab.com для сборки и деплой на ваш сервер через SSH. Если нужно больше контроля: настройте Specific Runner на своем сервере.

Шаг 1: Настройка SSH доступа для CI

GitLab CI будет подключаться к серверу по SSH для деплоя. Настроим SSH ключи:

# На сервере: создаем пользователя для деплоя (опционально)
sudo adduser deployer
sudo usermod -aG sudo deployer
 
# Добавляем в sudoers для выполнения команд без пароля (если нужно)
# Для NPM обычно не требуется, так как он работает через Docker
# Для обычного Nginx:
# echo "deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx, /usr/bin/pm2" | sudo tee /etc/sudoers.d/deployer
 
# Переключаемся на deployer
su - deployer
 
# Создаем SSH ключи (на локальной машине или в GitLab CI/CD Settings)
ssh-keygen -t ed25519 -C "gitlab-ci@potapov.me" -f ~/.ssh/gitlab_ci_ed25519
 
# Копируем публичный ключ на сервер
# На сервере (под deployer):
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Вставляем содержимое gitlab_ci_ed25519.pub
chmod 600 ~/.ssh/authorized_keys

Шаг 2: Добавление секретов в GitLab

Переходим в GitLab: Settings → CI/CD → Variables и добавляем:

КлючЗначениеЗащищеноМаскировано
SSH_PRIVATE_KEYСодержимое gitlab_ci_ed25519
SSH_HOSTIP или домен сервера
SSH_USERdeployer
DEPLOY_PATH/var/www/potapov.me
NODE_ENVproduction

Безопасность: Включайте "Protected" для production переменных (доступны только в защищенных ветках) и "Masked" для секретов (не показываются в логах).

Для multi-environment добавляем переменные с Environment scope:

  • production переменные доступны только при деплое в production
  • staging переменные доступны только при деплое в staging

Шаг 3: Создание .gitlab-ci.yml

Создаем файл .gitlab-ci.yml в корне проекта:

# .gitlab-ci.yml
# GitLab CI/CD pipeline для деплоя Next.js приложения
 
# Глобальные настройки
image: node:20-alpine
 
# Стадии pipeline
stages:
  - install
  - lint
  - test
  - build
  - deploy
  - healthcheck
  - rollback
 
# Кэширование node_modules для ускорения
cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/
    - .npm/
 
# Переменные окружения
variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"
  ARTIFACT_COMPRESSION_LEVEL: "fast"
  CACHE_COMPRESSION_LEVEL: "fast"
 
# Шаблон для job с Node.js
.node_job:
  before_script:
    - node -v
    - npm -v
 
# === STAGE: Install Dependencies ===
install_dependencies:
  extends: .node_job
  stage: install
  script:
    - echo "Installing dependencies..."
    - npm ci --prefer-offline --no-audit
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour
  only:
    - main
    - develop
    - merge_requests
 
# === STAGE: Lint ===
lint:
  extends: .node_job
  stage: lint
  dependencies:
    - install_dependencies
  script:
    - echo "Running ESLint..."
    - npm run lint
  only:
    - main
    - develop
    - merge_requests
 
# === STAGE: Test (опционально) ===
# Раскомментируйте, если есть тесты
# test:
#   extends: .node_job
#   stage: test
#   dependencies:
#     - install_dependencies
#   script:
#     - echo "Running tests..."
#     - npm run test
#   coverage: '/Statements\s*:\s*(\d+\.\d+)%/'
#   only:
#     - main
#     - develop
#     - merge_requests
 
# === STAGE: Build ===
build:
  extends: .node_job
  stage: build
  dependencies:
    - install_dependencies
  script:
    - echo "Building Next.js application..."
    - npm run build
  artifacts:
    paths:
      - .next/
      - public/
    expire_in: 1 hour
  only:
    - main
    - develop
 
# === STAGE: Deploy to Production ===
deploy_production:
  stage: deploy
  image: alpine:latest
  dependencies:
    - build
  before_script:
    # Установка SSH клиента
    - apk add --no-cache openssh-client bash git
 
    # Настройка SSH
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
 
  script:
    - echo "Deploying to production server..."
 
    # Создаем бэкап текущей версии
    - |
      ssh $SSH_USER@$SSH_HOST "
        cd $DEPLOY_PATH &&
        if [ -d .next ]; then
          echo 'Creating backup...' &&
          BACKUP_NAME=backup-\$(date +%Y%m%d-%H%M%S) &&
          mkdir -p backups &&
          tar -czf backups/\$BACKUP_NAME.tar.gz .next package.json package-lock.json &&
          echo 'Backup created: '\$BACKUP_NAME &&
 
          # Оставляем только последние 5 бэкапов
          cd backups &&
          ls -t | tail -n +6 | xargs -r rm &&
          cd ..
        fi
      "
 
    # Деплоим через git pull на сервере
    - echo "Deploying via git pull..."
    - |
      ssh $SSH_USER@$SSH_HOST "
        cd $DEPLOY_PATH &&
        git fetch origin &&
        git reset --hard origin/main &&
        echo 'Code updated from git'
      "
 
    # Устанавливаем зависимости, билдим и перезапускаем PM2
    - |
      ssh $SSH_USER@$SSH_HOST "
        cd $DEPLOY_PATH &&
        echo 'Installing dependencies...' &&
        npm ci --prefer-offline &&
 
        echo 'Building application...' &&
        npm run build &&
 
        echo 'Reloading PM2...' &&
        pm2 reload ecosystem.config.cjs --update-env &&
 
        echo 'Waiting for app to start...' &&
        sleep 5 &&
 
        pm2 status
      "
 
    - echo "Deployment completed successfully!"
 
  environment:
    name: production
    url: https://potapov.me
 
  only:
    - main
 
# === STAGE: Deploy to Staging ===
deploy_staging:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client bash git
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY_STAGING" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan -H $SSH_HOST_STAGING >> ~/.ssh/known_hosts
 
  script:
    - echo "Deploying to staging server..."
    - |
      ssh $SSH_USER_STAGING@$SSH_HOST_STAGING "
        cd $DEPLOY_PATH_STAGING &&
        git fetch origin &&
        git reset --hard origin/develop &&
        npm ci &&
        npm run build &&
        pm2 reload ecosystem.config.staging.js --update-env
      "
 
  environment:
    name: staging
    url: https://staging.potapov.me
 
  only:
    - develop
 
# === STAGE: Health Check ===
healthcheck_production:
  stage: healthcheck
  image: alpine:latest
  before_script:
    - apk add --no-cache curl
 
  script:
    - echo "Checking application health..."
    - |
      for i in {1..10}; do
        if curl -f -s https://potapov.me/api/health > /dev/null; then
          echo "✅ Health check passed!"
          exit 0
        else
          echo "⏳ Waiting for app to be ready (attempt $i/10)..."
          sleep 5
        fi
      done
      echo "❌ Health check failed after 10 attempts"
      exit 1
 
  dependencies:
    - deploy_production
 
  only:
    - main
 
# === STAGE: Rollback (manual) ===
rollback_production:
  stage: rollback
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
 
  script:
    - echo "Rolling back to previous version..."
    - |
      ssh $SSH_USER@$SSH_HOST "
        cd $DEPLOY_PATH &&
 
        # Находим последний бэкап
        LATEST_BACKUP=\$(ls -t backups/*.tar.gz | head -1) &&
 
        if [ -z \"\$LATEST_BACKUP\" ]; then
          echo '❌ No backups found!'
          exit 1
        fi &&
 
        echo 'Restoring from backup: '\$LATEST_BACKUP &&
 
        # Восстанавливаем из бэкапа
        tar -xzf \$LATEST_BACKUP &&
 
        # Перезапускаем PM2
        pm2 reload ecosystem.config.cjs &&
 
        echo '✅ Rollback completed!'
      "
 
  when: manual
  only:
    - main

Что делает этот pipeline:

  1. Install: устанавливает зависимости с кэшированием для ускорения последующих запусков
  2. Lint: проверяет код на ошибки стиля
  3. Build: собирает Next.js приложение и сохраняет артефакты
  4. Deploy: подключается по SSH, создает бэкап, синхронизирует код, устанавливает зависимости, перезапускает PM2
  5. Health Check: проверяет доступность приложения через /api/health
  6. Rollback: ручная стадия для отката к предыдущей версии из бэкапа

Оптимизации pipeline

1. Кэширование Docker слоев (если используете Docker)

build_docker:
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker pull $CI_REGISTRY_IMAGE:latest || true
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

2. Parallel jobs для ускорения

# Запускаем lint и test параллельно
lint:
  stage: test
  script:
    - npm run lint
 
test:unit:
  stage: test
  script:
    - npm run test:unit
 
test:e2e:
  stage: test
  script:
    - npm run test:e2e

3. Условный деплой с approval

deploy_production:
  stage: deploy
  when: manual # Требует ручного подтверждения
  only:
    - main

Секреты

Безопасное управление секретами: критически важная часть CI/CD.

Уровни секретов

  1. GitLab CI/CD Variables: для инфраструктурных секретов (SSH ключи, API токены)
  2. Environment Variables на сервере: для application secrets (.env файлы)
  3. HashiCorp Vault / AWS Secrets Manager: для enterprise-решений

Настройка .env через GitLab CI

Добавляем в GitLab Variables:

DATABASE_URL=postgresql://user:pass@localhost:5432/db
NEXT_PUBLIC_API_URL=https://api.potapov.me
SECRET_KEY=your-secret-key

Обновляем deploy stage в .gitlab-ci.yml:

deploy_production:
  script:
    # ... previous steps ...
 
    # Создаем .env файл из GitLab Variables
    - |
      ssh $SSH_USER@$SSH_HOST "
        cd $DEPLOY_PATH &&
        cat > .env << EOF
        NODE_ENV=production
        DATABASE_URL=$DATABASE_URL
        NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
        SECRET_KEY=$SECRET_KEY
        EOF
      "
 
    # Устанавливаем права доступа
    - ssh $SSH_USER@$SSH_HOST "chmod 600 $DEPLOY_PATH/.env"

Безопасность .env файлов:

  • Никогда не коммитьте .env в репозиторий
  • Используйте .env.example с пустыми значениями как шаблон
  • Ограничьте права доступа: chmod 600 .env
  • Используйте разные секреты для staging и production
  • Ротируйте секреты регулярно

Защита от утечек секретов

# Добавляем маскирование секретов в логах
variables:
  GIT_STRATEGY: clone
  GIT_DEPTH: 1 # Shallow clone для ускорения
 
before_script:
  # Маскируем чувствительные данные в логах
  - export DATABASE_URL="***MASKED***"

Мониторинг

PM2 мониторинг

Проверяем статус приложения:

# Статус всех процессов
pm2 status
 
# Детальная информация
pm2 show potapov-me
 
# Мониторинг в реальном времени
pm2 monit
 
# Логи
pm2 logs potapov-me --lines 100
 
# Очистка старых логов
pm2 flush

Настройка PM2 Plus (опционально)

PM2 Plus: SaaS платформа для мониторинга, алертов и профилирования:

# Регистрация (бесплатный план: 1 сервер, 4 приложения)
pm2 plus
 
# Подключение к PM2 Plus
pm2 link <secret_key> <public_key>

Возможности:

  • Реалтайм мониторинг CPU/RAM/Event Loop
  • Алерты в Slack/Email при ошибках
  • Exception tracking
  • Custom metrics
  • Performance profiling

Health checks через cron

Создаем скрипт для мониторинга:

# /var/www/potapov.me/scripts/healthcheck.sh
#!/bin/bash
 
URL="https://potapov.me/api/health"
LOG="/var/www/potapov.me/logs/healthcheck.log"
 
response=$(curl -s -o /dev/null -w "%{http_code}" $URL)
 
if [ $response -eq 200 ]; then
  echo "$(date): ✅ Health check passed" >> $LOG
else
  echo "$(date): ❌ Health check failed (HTTP $response)" >> $LOG
 
  # Отправляем алерт (например, в Telegram)
  curl -s -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" \
    -d chat_id=<CHAT_ID> \
    -d text="🚨 potapov.me is down! HTTP $response"
 
  # Пытаемся перезапустить PM2
  pm2 restart potapov-me
fi

Добавляем в crontab:

chmod +x /var/www/potapov.me/scripts/healthcheck.sh
 
# Проверка каждые 5 минут
crontab -e
*/5 * * * * /var/www/potapov.me/scripts/healthcheck.sh

Интеграция с внешними сервисами

UptimeRobot (бесплатно):

  • 50 мониторов
  • Проверка каждые 5 минут
  • Алерты в Email/Slack/Telegram

Healthchecks.io (бесплатно):

  • 20 проверок
  • Cron monitoring
  • Интеграции с 50+ сервисами

Staging и production

Настроим окружения staging и production с разными конфигурациями.

Структура окружений

/var/www/
├── potapov.me/          # Production
│   ├── .env
│   ├── ecosystem.config.cjs
│   └── ...
└── staging.potapov.me/  # Staging
    ├── .env
    ├── ecosystem.config.cjs (порт 3001)
    └── ...

Ecosystem для staging

// ecosystem.config.staging.js
module.exports = {
  apps: [
    {
      name: "potapov-me-staging",
      cwd: "/var/www/staging.potapov.me",
      script: "node_modules/next/dist/bin/next",
      args: "start -p 3001",
      env: {
        NODE_ENV: "production",
        PORT: 3001,
        STAGING: "true",
      },
      exec_mode: "cluster",
      instances: 1, // Меньше инстансов для staging
      autorestart: true,
      max_memory_restart: "300M",
    },
  ],
};

Настройка proxy для staging

В Nginx Proxy Manager добавьте еще один Proxy Host:

  • Domain: staging.potapov.me
  • Forward to: localhost:3001 (или IP сервера со staging)
  • SSL: запросите отдельный сертификат для staging

Если используете обычный Nginx:

# /etc/nginx/sites-available/staging.potapov.me
server {
    listen 80;
    server_name staging.potapov.me;
 
    location / {
        proxy_pass http://localhost:3001;
        # ... остальные настройки как для production
    }
}

GitLab CI для multi-environment

Обновляем .gitlab-ci.yml:

# Deploy to staging (ветка develop)
deploy_staging:
  stage: deploy
  script:
    - ssh $SSH_USER@$SSH_HOST "cd /var/www/staging.potapov.me && git pull origin develop && npm ci && npm run build && pm2 reload ecosystem.config.staging.js"
  environment:
    name: staging
    url: https://staging.potapov.me
  only:
    - develop
 
# Deploy to production (ветка main)
deploy_production:
  stage: deploy
  script:
    - ssh $SSH_USER@$SSH_HOST "cd /var/www/potapov.me && git pull origin main && npm ci && npm run build && pm2 reload ecosystem.config.cjs"
  environment:
    name: production
    url: https://potapov.me
  only:
    - main
  when: manual # Ручной деплой в production

Workflow:

  1. Разработка → коммит в feature/* ветку → создание MR
  2. Review → мерж в develop → автоматический деплой на staging
  3. Тестирование на staging → мерж develop в main
  4. Ручное подтверждение деплоя в production через GitLab UI

Rollback

Автоматический rollback при ошибках

Добавляем в .gitlab-ci.yml:

healthcheck_production:
  stage: healthcheck
  script:
    - |
      if ! curl -f https://potapov.me/api/health; then
        echo "Health check failed! Triggering rollback..."
        # Вызываем rollback job
        curl -X POST \
          -F token=$CI_JOB_TOKEN \
          -F ref=main \
          -F "variables[TRIGGER_ROLLBACK]=true" \
          https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/trigger/pipeline
        exit 1
      fi

Ручной rollback через GitLab UI

Pipeline с кнопкой rollback:

rollback_production:
  stage: rollback
  when: manual
  script:
    - |
      ssh $SSH_USER@$SSH_HOST "
        cd $DEPLOY_PATH &&
        LATEST_BACKUP=\$(ls -t backups/*.tar.gz | head -1) &&
        tar -xzf \$LATEST_BACKUP &&
        pm2 reload ecosystem.config.cjs
      "

Кнопка появится в GitLab → CI/CD → Pipelines → [pipeline] → Rollback

Blue-Green Deployment

Для минимизации простоя используем Blue-Green деплой:

# На сервере: две копии приложения
/var/www/potapov.me-blue/   # Текущая версия
/var/www/potapov.me-green/  # Новая версия
 
# Nginx переключает между ними
upstream backend {
    server localhost:3000;  # blue
    # server localhost:3001;  # green
}

Скрипт переключения:

#!/bin/bash
# switch-deployment.sh
 
CURRENT=$(readlink /var/www/potapov.me)
 
if [[ $CURRENT == *"blue"* ]]; then
  NEW_COLOR="green"
  NEW_PORT=3001
else
  NEW_COLOR="blue"
  NEW_PORT=3000
fi
 
echo "Switching from $CURRENT to $NEW_COLOR..."
 
# Деплоим в неактивное окружение
cd /var/www/potapov.me-$NEW_COLOR
git pull
npm ci
npm run build
 
# Запускаем новую версию
pm2 start ecosystem.config.cjs
 
# Проверяем health
if curl -f http://localhost:$NEW_PORT/api/health; then
  # Обновляем symlink
  ln -sfn /var/www/potapov.me-$NEW_COLOR /var/www/potapov.me
 
  # Перезагружаем Nginx
  sudo nginx -s reload
 
  echo "✅ Switched to $NEW_COLOR"
else
  echo "❌ Health check failed, keeping $CURRENT"
  pm2 stop ecosystem.config.cjs
  exit 1
fi

Альтернативы и расширения

Описанная в статье связка GitLab CI + PM2 + NPM: не единственный вариант. Вот несколько альтернатив для разных сценариев:

Альтернативы Nginx Proxy Manager

Traefik

Traefik: современный reverse proxy с автоматической конфигурацией через Docker labels.

Преимущества:

  • Автоматическое обнаружение сервисов через Docker/Kubernetes
  • Встроенный Let's Encrypt
  • Поддержка HTTP/2, HTTP/3, gRPC
  • Мощная система middleware (rate limiting, authentication, etc.)

Недостатки:

  • Сложнее в настройке для простых случаев
  • Требует Docker для полноценной работы

Когда использовать: Если у вас микросервисная архитектура с Docker/Kubernetes.

Caddy

Caddy: минималистичный веб-сервер с автоматическим HTTPS.

Преимущества:

  • Автоматический HTTPS из коробки (Let's Encrypt)
  • Простейшая конфигурация (Caddyfile)
  • Встроенный reverse proxy

Пример конфигурации:

potapov.me {
    reverse_proxy localhost:3000
}

Когда использовать: Если хотите максимально простую настройку без веб-интерфейса.

All-in-One решения (альтернативы Vercel)

Если не хотите настраивать CI/CD вручную, есть open-source платформы "из коробки":

Coolify

Coolify: self-hosted альтернатива Vercel/Netlify/Heroku.

Что умеет:

  • Git push to deploy (поддержка GitHub, GitLab, Bitbucket)
  • Автоматические SSL сертификаты
  • Database management (PostgreSQL, MySQL, Redis, MongoDB)
  • Автоматические бэкапы
  • Встроенный мониторинг
  • Web UI для управления

Плюсы:

  • Установка за 5 минут
  • Поддержка Next.js, Node.js, PHP, Python, Ruby, Go, Rust
  • Бесплатный и open-source

Минусы:

  • Требует больше ресурсов (Docker overhead)
  • Меньше контроля над инфраструктурой

Установка:

curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

Из личного опыта: Тестировал Coolify полгода назад (весна 2025). Концепция прикольная, UI удобный, но платформа оказалась сильно глючной и сырой:

  • Деплои периодически падали без внятных ошибок
  • База данных несколько раз слетала после обновлений
  • Приходилось перезапускать Docker контейнеры вручную

Возможно, сейчас стало лучше (активная разработка), но для критичных проектов пока не рискну. Для экспериментов и некритичных сервисов: вполне можно попробовать.

CapRover

CapRover: PaaS платформа для деплоя приложений одной командой.

Особенности:

  • One-click apps (WordPress, Ghost, Grafana, etc.)
  • Multi-server deployment
  • Load balancing
  • Автоматический HTTPS

Когда использовать: Если нужно быстро развернуть несколько приложений на одном сервере.

Dokploy

Dokploy: новый open-source аналог Vercel (появился в 2024).

Что интересного:

  • Современный UI (похож на Vercel)
  • Поддержка монорепозиториев
  • Preview deployments для pull requests
  • Edge functions
  • Встроенная аналитика

Статус: Активная разработка, но уже можно использовать в production.

GitHub: dokploy/dokploy

Рекомендации по выбору

СценарийРекомендация
Хобби-проект, 1-2 приложенияБазовая связка (GitLab CI + PM2 + NPM/Caddy)
Множество приложений на одном сервереNginx Proxy Manager или Traefik
Хочу "как Vercel", но self-hostedCoolify или Dokploy
Микросервисы с DockerTraefik + Kubernetes/Docker Swarm
Максимальная простотаCapRover
Полный контроль и гибкостьРучная настройка (эта статья)

Мой выбор для potapov.me: Базовая связка без излишеств: GitLab CI, git pull, PM2, NPM. Работает стабильно, не требует обслуживания, достаточно для хобби-проекта.

Если сломалось

Частые проблемы и решения

1. PM2 не запускается после перезагрузки сервера

Симптомы: После reboot приложение не работает, pm2 status показывает пустой список.

Решение:

# Проверяем статус systemd сервиса PM2
systemctl status pm2-$USER
 
# Если сервис не активен, настраиваем автозапуск
pm2 startup systemd -u $USER --hp $HOME
# Выполните команду с sudo, которую выведет PM2
 
# Сохраняем текущий список процессов
pm2 save
 
# Тестируем
sudo reboot
# После перезагрузки проверяем
pm2 status

2. GitLab CI Pipeline падает с "Permission denied" при SSH

Симптомы:

Permission denied (publickey).
fatal: Could not read from remote repository.

Решение:

# 1. Проверьте, что SSH ключ добавлен в GitLab Variables
# Settings → CI/CD → Variables → SSH_PRIVATE_KEY
 
# 2. Убедитесь, что публичный ключ добавлен на сервере
cat ~/.ssh/authorized_keys
 
# 3. Проверьте права на .ssh директорию
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
 
# 4. Проверьте SSH подключение вручную
ssh -i ~/.ssh/gitlab_ci_key user@server
 
# 5. Добавьте -vvv для диагностики
ssh -vvv user@server

3. Health check fails после деплоя

Симптомы: Pipeline падает на стадии healthcheck.

Диагностика:

# 1. Проверяем, что приложение запущено
pm2 status
pm2 logs app-name --lines 50
 
# 2. Проверяем, слушает ли процесс нужный порт
sudo netstat -tlnp | grep 3000
# или
sudo lsof -i :3000
 
# 3. Проверяем health endpoint локально
curl http://localhost:3000/api/health
 
# 4. Проверяем с внешнего IP
curl http://YOUR_IP:3000/api/health
 
# 5. Проверяем логи Nginx/NPM
docker logs nginx-proxy-manager_app_1

Типичные причины:

  • Приложение упало при старте (смотрите pm2 logs)
  • Неправильный порт в ecosystem.config.cjs
  • Health endpoint не отвечает (проверьте код API route)
  • Firewall блокирует порт (проверьте ufw status)

4. Build fails: "FATAL ERROR: Reached heap limit"

Симптомы: Next.js build падает с ошибкой памяти.

Решение:

# 1. Проверьте доступную память
free -h
 
# 2. Создайте swap файл (если нет)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
 
# 3. Сделайте swap постоянным
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
 
# 4. Или увеличьте heap для Node.js в package.json
"scripts": {
  "build": "NODE_OPTIONS='--max-old-space-size=2048' next build"
}

5. git pull падает с конфликтами или деплой очень медленный

Симптомы: Git жалуется на локальные изменения или деплой занимает > 10 минут.

Решение:

# Вариант 1: Жесткий reset (удаляет все локальные изменения)
ssh user@server "
  cd /var/www/app &&
  git fetch origin &&
  git reset --hard origin/main &&
  git clean -fd
"
 
# Вариант 2: Stash локальных изменений
ssh user@server "
  cd /var/www/app &&
  git stash &&
  git pull origin main
"
 
# Вариант 3: Если репозиторий большой, используйте shallow clone
# При первичной настройке:
git clone --depth 1 https://gitlab.com/user/repo.git /var/www/app
 
# При деплое:
ssh user@server "
  cd /var/www/app &&
  git fetch --depth 1 &&
  git reset --hard origin/main
"

6. NPM не получает SSL сертификат

Симптомы: "Failed to obtain certificate" в логах NPM.

Решение:

# 1. Проверьте, что домен указывает на ваш IP
dig potapov.me +short
nslookup potapov.me
 
# 2. Проверьте, что порты 80 и 443 открыты
sudo ufw status
sudo netstat -tlnp | grep ':80\|:443'
 
# 3. Проверьте логи NPM
docker logs nginx-proxy-manager_app_1 --tail 100
 
# 4. Попробуйте получить сертификат вручную через certbot
sudo certbot certonly --standalone -d potapov.me

Полезные команды для диагностики

# === PM2 ===
pm2 status                      # Статус всех процессов
pm2 logs app-name --lines 100   # Логи приложения
pm2 monit                       # Мониторинг в реальном времени
pm2 describe app-name           # Детальная информация
pm2 reset app-name              # Сброс счетчиков рестартов
 
# === Проверка портов ===
sudo netstat -tlnp              # Все слушающие порты
sudo lsof -i :3000              # Процесс на порту 3000
nc -zv localhost 3000           # Проверка доступности порта
 
# === Логи системы ===
journalctl -u pm2-$USER -f      # Логи PM2 systemd сервиса
journalctl -u nginx -f          # Логи Nginx (если не Docker)
tail -f /var/log/syslog         # Системные логи
 
# === Docker (для NPM) ===
docker ps                       # Запущенные контейнеры
docker logs <container> -f      # Логи контейнера
docker exec -it <container> sh  # Зайти внутрь контейнера
 
# === Disk space ===
df -h                           # Свободное место на дисках
du -sh /var/www/*               # Размер директорий
du -sh node_modules .next       # Размер артефактов
 
# === Memory ===
free -h                         # Память и swap
htop                            # Интерактивный мониторинг

Что стоит

potapov.me так и крутится. Один внешний IP, PM2, Nginx Proxy Manager, GitLab CI. Reload без дырки, секреты в Variables, rollback ручной и по health check.

Дальше, если понадобится: Docker, миграции в pipeline, Playwright, budget на бандл, CDN.

GitLab CI, PM2, Next.js deploy, Nginx Proxy Manager.