DevOps

Деплой Nuxt 4 приложения на VPS с GitLab CI/CD

BorisBoris
|
6 января 2026 г.
|
18 мин чтения
|
118 просмотров
Деплой Nuxt 4 приложения на VPS с GitLab CI/CD

В этой статье мы настроим автоматический деплой Nuxt 4 приложения на VPS сервер. После настройки каждый push в ветку main будет автоматически собирать и разворачивать ваше приложение.
Статья составлена из моих заметок и опыта настройки этого сайта (собственно, поэтому она и первая).
Я не эксперт в DevOps, поэтому возможно какие-то моменты понимаю/делаю не правильно. Замечания и комментарии приветствуются.
Чуть позже добавлю заметку о том как я выбирал хостера для VPS и где в итоге арендовал.

Итак, что мы будем использовать

  • GitLab CI/CD — автоматическая сборка и деплой при каждом коммите
  • Docker — контейнеризация приложения для единообразного окружения
  • Nginx — веб-сервер и reverse proxy с SSL
  • MongoDB — база данных
  • Let's Encrypt — бесплатные SSL сертификаты

Требования

  • VPS сервер с Ubuntu 20.04+ (минимум 1GB RAM, рекомендуется 2GB).
    В моем случае сервером выступил:
root@borisbogatov:~# uname -a
Linux borisbogatov.ru 6.12.41+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.41-1 (2025-08-12) x86_64 GNU/Linux
  • Домен, направленный на IP вашего сервера (A-запись в DNS)
  • Аккаунт на GitLab с репозиторием вашего проекта
  • Базовые знания командной строки Linux

Архитектура

GitLab CI/CD (Build & Push)
         ↓ SSH Deploy
+-------- VPS ------------------+
|  Nginx (443/80) + SSL         |
|      ↓ proxy                  |
|  Nuxt App (:3000)             |
|      ↓                        |
|  MongoDB (:27017)             |
+-------------------------------+

Как это работает:

  1. Вы делаете git push в ветку main
  2. GitLab CI автоматически собирает Docker-образ вашего приложения
  3. Образ загружается в GitLab Container Registry
  4. GitLab подключается к вашему VPS по SSH
  5. На VPS скачивается новый образ и перезапускается приложение

Необходимые файлы

Создайте эти файлы в корне вашего проекта. Если файлы уже существуют — замените их содержимое.

Dockerfile

Dockerfile описывает, как собирать Docker-образ вашего приложения. Мы используем многоэтапную сборку (multi-stage build) для минимизации размера финального образа.

Dockerfile
# =============================================
# Stage 1: Dependencies
# Устанавливаем только production-зависимости
# =============================================
FROM node:22-alpine AS deps

WORKDIR /app

# Копируем файлы с зависимостями
COPY package*.json ./

# Устанавливаем только production-зависимости (без devDependencies)
RUN npm ci --omit=dev

# =============================================
# Stage 2: Build
# Собираем приложение со всеми зависимостями
# =============================================
FROM node:22-alpine AS builder

WORKDIR /app

COPY package*.json ./
# Устанавливаем ВСЕ зависимости (включая dev) для сборки
RUN npm ci

# Копируем исходный код
COPY . .

# Собираем Nuxt приложение
RUN npm run build

# =============================================
# Stage 3: Production Runtime
# Минимальный образ только для запуска
# =============================================
FROM node:22-alpine AS runner

WORKDIR /app

# Создаём непривилегированного пользователя для безопасности
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nuxtjs

# Копируем только собранное приложение (папка .output)
COPY --from=builder /app/.output ./.output

# Создаём папку для загрузок и даём права
RUN mkdir -p /app/.output/public/uploads && \
    chown -R nuxtjs:nodejs /app/.output

# Запускаем от непривилегированного пользователя
USER nuxtjs

# Открываем порт 3000
EXPOSE 3000

# Переменные окружения
ENV HOST=0.0.0.0
ENV PORT=3000
ENV NODE_ENV=production

# Команда запуска приложения
CMD ["node", ".output/server/index.mjs"]

docker-compose.prod.yml

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

docker-compose.prod.yml
services:
  # =============================================
  # Nuxt Application
  # Ваше приложение
  # =============================================
  app:
    # Образ берётся из GitLab Container Registry
    image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA:-latest}
    container_name: blog-app
    # Автоматический перезапуск при падении
    restart: unless-stopped
    environment:
      # Подключение к MongoDB (mongo — имя сервиса в этом файле)
      - NUXT_MONGODB_URI=mongodb://mongo:27017
      - NUXT_MONGO_DB_NAME=${MONGO_DB}
      # JWT секреты для аутентификации
      - NUXT_JWT_SECRET=${JWT_SECRET}
      - NUXT_JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
      # URL сайта для генерации ссылок
      - NUXT_PUBLIC_SITE_URL=${SITE_URL}
      - HOST=0.0.0.0
      - PORT=3000
    volumes:
      # Папка для загруженных файлов (сохраняется между перезапусками)
      - uploads_data:/app/.output/public/uploads
    networks:
      - blog-network
    # Ждём пока MongoDB будет готова
    depends_on:
      mongo:
        condition: service_healthy
    # Проверка здоровья приложения
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # =============================================
  # MongoDB
  # База данных
  # =============================================
  mongo:
    image: mongo:7
    container_name: blog-mongo
    restart: unless-stopped
    environment:
      - MONGO_INITDB_DATABASE=${MONGO_DB}
    volumes:
      # Данные базы сохраняются между перезапусками
      - mongo_data:/data/db
      - mongo_config:/data/configdb
    networks:
      - blog-network
    # Проверка здоровья MongoDB
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  # =============================================
  # Nginx Reverse Proxy
  # Веб-сервер: принимает запросы и проксирует на приложение
  # =============================================
  nginx:
    image: nginx:alpine
    container_name: blog-nginx
    restart: unless-stopped
    ports:
      # Открываем порты для HTTP и HTTPS
      - "80:80"
      - "443:443"
    volumes:
      # Конфигурационные файлы nginx (только чтение)
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      # SSL сертификаты от Let's Encrypt
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
      # Папка с загрузками для раздачи напрямую
      - uploads_data:/var/www/uploads:ro
    networks:
      - blog-network
    depends_on:
      - app

  # =============================================
  # Certbot for SSL
  # Автоматическое обновление SSL сертификатов
  # =============================================
  certbot:
    image: certbot/certbot
    container_name: blog-certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    # Бесконечный цикл проверки и обновления сертификатов каждые 12 часов
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
    networks:
      - blog-network

# Внутренняя сеть для связи контейнеров
networks:
  blog-network:
    driver: bridge

# Именованные тома для сохранения данных
volumes:
  mongo_data:
    driver: local
  mongo_config:
    driver: local
  uploads_data:
    driver: local

.gitlab-ci.yml

Этот файл описывает pipeline — последовательность действий, которые GitLab выполняет при каждом push. У нас два этапа: сборка образа и деплой на сервер.
Более подробно про файл в статье CI/CD в GitLab: руководство для начинающих

.gitlab-ci.yml
# Этапы pipeline (выполняются последовательно)
stages:
  - build   # Сборка Docker-образа
  - deploy  # Деплой на VPS

# Глобальные переменные
variables:
  DOCKER_TLS_CERTDIR: "/certs"
  # Теги для Docker-образов
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA      # Уникальный тег по коммиту
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest           # Тег "latest"

# =============================================
# Build Stage
# Сборка Docker-образа и загрузка в registry
# =============================================
build:
  stage: build
  # Используем Docker-in-Docker для сборки
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    # Авторизуемся в GitLab Container Registry
    # CI_REGISTRY_USER и CI_REGISTRY_PASSWORD — встроенные переменные GitLab
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - echo "Building Docker image..."
    # Собираем образ с двумя тегами
    - docker build -t $IMAGE_TAG -t $IMAGE_LATEST .
    - echo "Pushing to registry..."
    # Загружаем оба тега в registry
    - docker push $IMAGE_TAG
    - docker push $IMAGE_LATEST
  # Запускать только для ветки main
  only:
    - main

# =============================================
# Deploy Stage
# Подключение к VPS и обновление приложения
# =============================================
deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    # Устанавливаем SSH клиент
    - apk add --no-cache openssh-client
    # Настраиваем SSH ключ из переменной GitLab
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    # Добавляем сервер в known_hosts (чтобы не было вопроса о подлинности)
    - ssh-keyscan -H $VPS_HOST >> ~/.ssh/known_hosts
  script:
    - echo "Deploying to VPS..."
    # Подключаемся к серверу и выполняем команды
    - |
      ssh $VPS_USER@$VPS_HOST << ENDSSH
        cd /opt/blog

        # Авторизуемся в GitLab Registry на сервере
        echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY

        # Экспортируем переменные для docker-compose
        export CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE
        export CI_COMMIT_SHA=$CI_COMMIT_SHA

        # Скачиваем новый образ
        docker-compose -f docker-compose.prod.yml pull app
        # Перезапускаем контейнеры
        docker-compose -f docker-compose.prod.yml up -d

        # Удаляем старые неиспользуемые образы
        docker image prune -f

        echo "Deploy completed!"
      ENDSSH
  # Информация об окружении для GitLab UI
  environment:
    name: production
    url: https://$SITE_DOMAIN
  only:
    - main
  # Раскомментируйте строку ниже для ручного деплоя (кнопка "Play" в GitLab)
  # when: manual

nginx/nginx.conf

Основной конфигурационный файл Nginx с оптимальными настройками производительности и безопасности.

nginx/nginx.conf
user nginx;
# Автоматически определяем количество воркеров по числу ядер CPU
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    # Максимум соединений на воркер
    worker_connections 1024;
    # Принимать несколько соединений за раз
    multi_accept on;
}

http {
    # MIME-типы для правильной отдачи файлов
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Формат логов
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;

    # Оптимизация отдачи файлов
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Сжатие ответов (уменьшает трафик)
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript
               application/xml application/xml+rss text/javascript application/x-javascript
               image/svg+xml;
    gzip_min_length 1000;

    # Лимиты на размер запросов (важно для загрузки файлов)
    client_max_body_size 10M;
    client_body_buffer_size 10K;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 32k;

    # Подключаем конфигурации сайтов
    include /etc/nginx/conf.d/*.conf;
}

nginx/conf.d/blog.conf

Конфигурация вашего сайта с SSL, кэшированием статики и проксированием запросов к приложению.

Важно: Замените YOUR_DOMAIN на ваш реальный домен перед использованием!

nginx/conf.d/blog.conf
# Upstream — группа серверов для проксирования
# app:3000 — это имя сервиса из docker-compose и его порт
upstream nuxt_app {
    server app:3000;
    # Поддерживать постоянные соединения для производительности
    keepalive 32;
}

# =============================================
# HTTP Server (порт 80)
# Перенаправляет все запросы на HTTPS
# =============================================
server {
    listen 80;
    listen [::]:80;
    server_name YOUR_DOMAIN www.YOUR_DOMAIN;

    # Путь для проверки Let's Encrypt (нужен для получения сертификата)
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Все остальные запросы перенаправляем на HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# =============================================
# HTTPS Server (порт 443)
# Основной сервер с SSL
# =============================================
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name YOUR_DOMAIN www.YOUR_DOMAIN;

    # SSL сертификаты от Let's Encrypt
    ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;

    # Настройки SSL сессий
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Современные протоколы и шифры
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # Заголовки безопасности
    add_header Strict-Transport-Security "max-age=63072000" always;  # HSTS
    add_header X-Frame-Options "SAMEORIGIN" always;                   # Защита от clickjacking
    add_header X-Content-Type-Options "nosniff" always;               # Защита от MIME-sniffing
    add_header X-XSS-Protection "1; mode=block" always;               # Защита от XSS
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # =============================================
    # Статические файлы (раздаём напрямую через Nginx)
    # =============================================

    # Загруженные пользователями файлы
    location /uploads/ {
        alias /var/www/uploads/;
        expires 30d;  # Кэширование на 30 дней
        add_header Cache-Control "public, immutable";
        access_log off;  # Не логируем запросы к статике
    }

    # Статика Nuxt (JS, CSS, шрифты)
    location /_nuxt/ {
        proxy_pass http://nuxt_app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        expires 1y;  # Долгое кэширование (файлы имеют хэш в имени)
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Служебные файлы
    location ~* ^/(favicon\.ico|robots\.txt|sitemap\.xml)$ {
        proxy_pass http://nuxt_app;
        expires 7d;
        add_header Cache-Control "public";
        access_log off;
    }

    # =============================================
    # API запросы
    # =============================================
    location /api/ {
        proxy_pass http://nuxt_app;
        proxy_http_version 1.1;
        # Заголовки для WebSocket (если используется)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        # Передаём реальный IP клиента
        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_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # =============================================
    # SSR страницы (все остальные запросы)
    # =============================================
    location / {
        proxy_pass http://nuxt_app;
        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_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Запрещаем доступ к скрытым файлам (начинаются с точки)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Шаг 1: Настройка GitLab CI/CD Variables

CI/CD Variables — это секретные переменные, которые GitLab передаёт в pipeline. Они не хранятся в коде и не видны в логах.

Как добавить:

  1. Откройте ваш проект в GitLab
  2. Перейдите в Settings → CI/CD
  3. Разверните секцию Variables
  4. Нажмите Add variable для каждой переменной
VariableОписаниеПример значенияProtectedMasked
SSH_PRIVATE_KEYПриватный SSH ключ для подключения к серверуСодержимое файла ~/.ssh/gitlab_deployYesNo
VPS_HOSTIP-адрес или домен вашего VPS123.45.67.89 или YOUR_DOMAINYesNo
VPS_USERПользователь для SSH подключенияdeployYesNo
JWT_SECRETСекрет для подписи access токеновСлучайная строка 32+ символовYesYes
JWT_REFRESH_SECRETСекрет для подписи refresh токеновСлучайная строка 32+ символовYesYes
MONGO_DBИмя базы данных MongoDBblogYesNo
SITE_URLПолный URL вашего сайтаhttps://YOUR_DOMAINYesNo
SITE_DOMAINДомен без протоколаYOUR_DOMAINYesNo

Генерация секретов:

Выполните эту команду дважды для получения JWT_SECRET и JWT_REFRESH_SECRET:

openssl rand -base64 32

Что означают флаги?

  • Protected — переменная доступна только в защищённых ветках (main)
  • Masked — значение скрывается в логах pipeline (используйте для паролей)

Шаг 2: Подготовка VPS

2.0 Установка Docker (если не установлен)

# Подключитесь к серверу как root
ssh root@SERVER_IP_ADDRESS

# Установите Docker
curl -fsSL https://get.docker.com | sh

# Установите Docker Compose
apt update && apt install -y docker-compose-plugin

# Проверьте установку
docker --version
docker compose version

2.1 Создание пользователя deploy

Для безопасности мы создадим отдельного пользователя для деплоя, а не будем использовать root.

# Выполните от root на VPS
# Создаём пользователя с домашней директорией и bash
useradd -m -s /bin/bash deploy

# Добавляем в группу docker (чтобы мог управлять контейнерами)
usermod -aG docker deploy

2.2 Настройка SSH ключа

SSH ключ позволяет GitLab подключаться к серверу без пароля.

На вашем локальном компьютере:

# Создаём новую пару ключей
ssh-keygen -t ed25519 -C "gitlab-deploy" -f ~/.ssh/gitlab_deploy

# При запросе passphrase просто нажмите Enter (пустой пароль)

Это создаст два файла:

  • ~/.ssh/gitlab_deploy — приватный ключ (НИКОМУ не показывайте!)
  • ~/.ssh/gitlab_deploy.pub — публичный ключ (добавляем на сервер)

На VPS (от root):

# Создаём папку .ssh для пользователя deploy
mkdir -p /home/deploy/.ssh

# Добавляем публичный ключ (замените на содержимое вашего gitlab_deploy.pub)
echo "ваш_публичный_ключ_из_gitlab_deploy.pub" >> /home/deploy/.ssh/authorized_keys

# Устанавливаем правильные права доступа
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh

Добавляем приватный ключ в GitLab:

  1. На локальном компьютере выведите содержимое приватного ключа:
    cat ~/.ssh/gitlab_deploy
    
  2. Скопируйте всё содержимое (включая -----BEGIN... и -----END...)
  3. Добавьте в GitLab как переменную SSH_PRIVATE_KEY

2.3 Создание структуры проекта на VPS

# От root на VPS
# Создаём директории для проекта
mkdir -p /opt/blog/{nginx/conf.d,certbot/{conf,www}}

# Передаём права пользователю deploy
chown -R deploy:deploy /opt/blog

Структура будет такой:

/opt/blog/
├── nginx/
│   ├── nginx.conf
│   └── conf.d/
│       └── blog.conf
├── certbot/
│   ├── conf/          # SSL сертификаты
│   └── www/           # Файлы для проверки Let's Encrypt
├── docker-compose.prod.yml
└── .env

2.4 Копирование файлов на VPS

Теперь нужно скопировать конфигурационные файлы с вашего компьютера на сервер.

Настройка SSH без пароля (рекомендуется)

Чтобы каждый раз не указывать ключ, настройте SSH config на вашем компьютере:

# Создайте или отредактируйте ~/.ssh/config
cat >> ~/.ssh/config << 'EOF'
Host SERVER_IP_ADDRESS
    IdentityFile ~/.ssh/gitlab_deploy
EOF

После этого команды ssh и scp будут автоматически использовать правильный ключ.

Для доступа под root (если нужно):

  1. Подключитесь к серверу как root с паролем
  2. Добавьте тот же публичный ключ:
mkdir -p ~/.ssh
echo "ваш_публичный_ключ" >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Копируем файлы:

# С вашего локального компьютера (из папки проекта)
scp docker-compose.prod.yml deploy@SERVER_IP_ADDRESS:/opt/blog/
scp -r nginx deploy@SERVER_IP_ADDRESS:/opt/blog/

Или с явным указанием ключа:

scp -i ~/.ssh/gitlab_deploy docker-compose.prod.yml deploy@SERVER_IP_ADDRESS:/opt/blog/
scp -i ~/.ssh/gitlab_deploy -r nginx deploy@SERVER_IP_ADDRESS:/opt/blog/

2.5 Создание .env на VPS

Файл .env содержит переменные окружения для docker-compose.

# Подключитесь к VPS
ssh deploy@SERVER_IP_ADDRESS

# Создайте файл .env
cat > /opt/blog/.env << 'EOF'
# Путь к образу в GitLab Registry (замените YOUR_GITLAB_USERNAME на ваш username)
CI_REGISTRY_IMAGE=registry.gitlab.com/YOUR_GITLAB_USERNAME/YOUR_PROJECT_NAME
CI_COMMIT_SHA=latest

# База данных
MONGO_DB=blog

# JWT секреты (используйте те же значения, что в GitLab Variables!)
JWT_SECRET=ваш-jwt-секрет-из-gitlab
JWT_REFRESH_SECRET=ваш-refresh-секрет-из-gitlab

# URL сайта
SITE_URL=https://YOUR_DOMAIN
EOF

Важно: CI_REGISTRY_IMAGE можно найти в GitLab: Deploy → Container Registry — там будет показан путь к registry вашего проекта.

2.6 Получение SSL сертификата

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

Предварительные требования:

  • Домен должен быть направлен на IP вашего сервера (A-запись в DNS)
  • Порт 80 должен быть открыт
  • Замените YOUR_DOMAIN и YOUR_EMAIL на реальные значения
# Подключитесь к VPS как root
ssh root@SERVER_IP_ADDRESS
cd /opt/blog

# Шаг 1: Создаём временный nginx конфиг (только для получения сертификата)
cat > nginx/conf.d/blog.conf << 'EOF'
server {
    listen 80;
    server_name YOUR_DOMAIN www.YOUR_DOMAIN;

    # Путь для проверки Let's Encrypt
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Временная заглушка
    location / {
        return 200 'Setting up SSL...';
        add_header Content-Type text/plain;
    }
}
EOF

# Шаг 2: Запускаем временный nginx
docker run -d --name temp-nginx -p 80:80 \
    -v $(pwd)/nginx/conf.d:/etc/nginx/conf.d:ro \
    -v $(pwd)/certbot/www:/var/www/certbot \
    nginx:alpine

# Шаг 3: Получаем сертификат
# ЗАМЕНИТЕ YOUR_DOMAIN и YOUR_EMAIL на реальные значения!
docker run --rm \
    -v $(pwd)/certbot/conf:/etc/letsencrypt \
    -v $(pwd)/certbot/www:/var/www/certbot \
    certbot/certbot certonly --webroot \
    -w /var/www/certbot \
    -d YOUR_DOMAIN -d www.YOUR_DOMAIN \
    --email YOUR_EMAIL \
    --agree-tos \
    --no-eff-email

# Шаг 4: Останавливаем временный nginx
docker stop temp-nginx && docker rm temp-nginx

После успешного получения сертификата:

Замените временный nginx/conf.d/blog.conf на полную версию из раздела "Необходимые файлы" (с SSL).

# Скопируйте полный конфиг с вашего компьютера
scp nginx/conf.d/blog.conf root@SERVER_IP_ADDRESS:/opt/blog/nginx/conf.d/

Не забудьте заменить YOUR_DOMAIN в файле на ваш реальный домен!

2.7 Настройка firewall

Firewall защищает сервер, разрешая только нужные подключения.

# От root на VPS
# Разрешаем SSH (чтобы не потерять доступ!)
ufw allow ssh

# Разрешаем HTTP и HTTPS
ufw allow http
ufw allow https

# Включаем firewall
ufw enable

# Проверяем статус
ufw status

Шаг 3: Первый деплой

Теперь всё готово для первого деплоя!

  1. Закоммитьте файлы (если ещё не сделали):
    git add Dockerfile docker-compose.prod.yml .gitlab-ci.yml nginx/
    git commit -m "Add Docker deployment configuration"
    git push origin main
    
  2. Следите за pipeline в GitLab:
    • Откройте ваш проект в GitLab
    • Перейдите в CI/CD → Pipelines
    • Вы увидите запущенный pipeline с двумя этапами
  3. Build stage (~3-5 минут):
    • Собирается Docker-образ
    • Загружается в GitLab Container Registry
  4. Deploy stage (~1 минута):
    • GitLab подключается к вашему VPS
    • Скачивает новый образ
    • Перезапускает контейнеры

Если включен ручной деплой (when: manual в .gitlab-ci.yml), нажмите кнопку "Play" рядом с deploy stage.


Шаг 4: Проверка

На VPS:

ssh deploy@SERVER_IP_ADDRESS
cd /opt/blog

# Проверяем статус контейнеров (все должны быть "Up")
docker-compose -f docker-compose.prod.yml ps

# Смотрим логи приложения
docker-compose -f docker-compose.prod.yml logs -f app

В браузере:

Откройте https://YOUR_DOMAIN — вы должны увидеть ваше приложение!


Полезные команды

Работа с контейнерами
# Все команды выполняются из /opt/blog на VPS

# Посмотреть логи приложения (Ctrl+C для выхода)
docker-compose -f docker-compose.prod.yml logs -f app

# Посмотреть логи всех сервисов
docker-compose -f docker-compose.prod.yml logs -f

# Перезапустить только приложение
docker-compose -f docker-compose.prod.yml restart app

# Полный перезапуск всех сервисов
docker-compose -f docker-compose.prod.yml down
docker-compose -f docker-compose.prod.yml up -d

# Проверить статус контейнеров
docker-compose -f docker-compose.prod.yml ps

# Зайти внутрь контейнера приложения
docker-compose -f docker-compose.prod.yml exec app sh

# Зайти в MongoDB
docker-compose -f docker-compose.prod.yml exec mongo mongosh
Бэкап и восстановление MongoDB
# Создать бэкап
docker-compose -f docker-compose.prod.yml exec mongo mongodump --db=blog --archive=/tmp/backup.gz --gzip
docker cp blog-mongo:/tmp/backup.gz ./backup_$(date +%Y%m%d).gz

# Скачать бэкап на локальный компьютер
scp deploy@SERVER_IP_ADDRESS:/opt/blog/backup_*.gz ./

# Восстановить из бэкапа
docker cp backup.gz blog-mongo:/tmp/backup.gz
docker-compose -f docker-compose.prod.yml exec mongo mongorestore --archive=/tmp/backup.gz --gzip
Обновление SSL сертификата

Сертификаты Let's Encrypt действуют 90 дней. Certbot в docker-compose автоматически их обновляет, но можно обновить вручную:

docker-compose -f docker-compose.prod.yml run --rm certbot renew
docker-compose -f docker-compose.prod.yml exec nginx nginx -s reload

Автообновление SSL

Для надёжности добавьте задачу в cron, которая будет проверять сертификаты раз в месяц:

# Откройте редактор crontab
crontab -e

# Добавьте эту строку (обновление 1-го числа каждого месяца в 00:00)
0 0 1 * * cd /opt/blog && docker-compose -f docker-compose.prod.yml run --rm certbot renew && docker-compose -f docker-compose.prod.yml exec nginx nginx -s reload

Решение проблем (Troubleshooting)

Pipeline падает на этапе build

Возможные причины:

  • Ошибка в Dockerfile
  • Не хватает памяти для сборки

Как проверить:

  1. Откройте лог упавшего job в GitLab
  2. Найдите строку с ошибкой

Частые решения:

  • Проверьте синтаксис Dockerfile
  • Убедитесь, что npm run build работает локально
Pipeline падает на этапе deploy

Возможные причины:

  • Неправильный SSH ключ
  • VPS недоступен
  • Неправильные права на файлы

Как проверить:

# Проверьте, что можете подключиться с локального компьютера
ssh -i ~/.ssh/gitlab_deploy deploy@SERVER_IP_ADDRESS

# Если не работает — проверьте:
# 1. Правильно ли скопирован публичный ключ на сервер
# 2. Правильные ли права на файлы (chmod 600, 700)
Приложение не запускается
# Смотрим логи приложения
docker-compose -f docker-compose.prod.yml logs app

# Частые причины:
# - Не может подключиться к MongoDB (проверьте логи mongo)
# - Не заданы переменные окружения (проверьте .env)
# - Ошибка в коде приложения
MongoDB не подключается
# Проверяем логи MongoDB
docker-compose -f docker-compose.prod.yml logs mongo

# Проверяем, что MongoDB отвечает
docker-compose -f docker-compose.prod.yml exec mongo mongosh --eval "db.adminCommand('ping')"
Nginx возвращает 502 Bad Gateway

502 означает, что Nginx не может связаться с приложением.

# Проверьте, запущено ли приложение
docker-compose -f docker-compose.prod.yml ps

# Проверьте health endpoint
docker-compose -f docker-compose.prod.yml exec app wget -qO- http://localhost:3000/api/health

# Если app не запущен — смотрите его логи
docker-compose -f docker-compose.prod.yml logs app
SSL не работает / сертификат не найден
# Проверьте, есть ли сертификаты
ls -la /opt/blog/certbot/conf/live/YOUR_DOMAIN/

# Если папки нет — сертификат не был получен
# Повторите шаг 2.6 (Получение SSL сертификата)

# Перевыпустить сертификат:
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
    --webroot -w /var/www/certbot \
    -d YOUR_DOMAIN -d www.YOUR_DOMAIN
Не хватает места на диске

Docker может занять много места старыми образами и контейнерами.

# Посмотреть использование диска Docker
docker system df

# Удалить неиспользуемые данные (образы, контейнеры, volumes)
docker system prune -a

# ⚠️ Осторожно: это удалит ВСЕ неиспользуемые данные!

Комментарии

Войдите, чтобы оставить комментарий

Пока нет комментариев. Будьте первым!