Деплой 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) |
+-------------------------------+
Как это работает:
- Вы делаете
git pushв веткуmain - GitLab CI автоматически собирает Docker-образ вашего приложения
- Образ загружается в GitLab Container Registry
- GitLab подключается к вашему VPS по SSH
- На 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. Они не хранятся в коде и не видны в логах.
Как добавить:
- Откройте ваш проект в GitLab
- Перейдите в Settings → CI/CD
- Разверните секцию Variables
- Нажмите Add variable для каждой переменной
| Variable | Описание | Пример значения | Protected | Masked |
|---|---|---|---|---|
SSH_PRIVATE_KEY | Приватный SSH ключ для подключения к серверу | Содержимое файла ~/.ssh/gitlab_deploy | Yes | No |
VPS_HOST | IP-адрес или домен вашего VPS | 123.45.67.89 или YOUR_DOMAIN | Yes | No |
VPS_USER | Пользователь для SSH подключения | deploy | Yes | No |
JWT_SECRET | Секрет для подписи access токенов | Случайная строка 32+ символов | Yes | Yes |
JWT_REFRESH_SECRET | Секрет для подписи refresh токенов | Случайная строка 32+ символов | Yes | Yes |
MONGO_DB | Имя базы данных MongoDB | blog | Yes | No |
SITE_URL | Полный URL вашего сайта | https://YOUR_DOMAIN | Yes | No |
SITE_DOMAIN | Домен без протокола | YOUR_DOMAIN | Yes | No |
Генерация секретов:
Выполните эту команду дважды для получения 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:
- На локальном компьютере выведите содержимое приватного ключа:
cat ~/.ssh/gitlab_deploy - Скопируйте всё содержимое (включая
-----BEGIN...и-----END...) - Добавьте в 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 (если нужно):
- Подключитесь к серверу как root с паролем
- Добавьте тот же публичный ключ:
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: Первый деплой
Теперь всё готово для первого деплоя!
- Закоммитьте файлы (если ещё не сделали):
git add Dockerfile docker-compose.prod.yml .gitlab-ci.yml nginx/ git commit -m "Add Docker deployment configuration" git push origin main - Следите за pipeline в GitLab:
- Откройте ваш проект в GitLab
- Перейдите в CI/CD → Pipelines
- Вы увидите запущенный pipeline с двумя этапами
- Build stage (~3-5 минут):
- Собирается Docker-образ
- Загружается в GitLab Container Registry
- 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
- Не хватает памяти для сборки
Как проверить:
- Откройте лог упавшего job в GitLab
- Найдите строку с ошибкой
Частые решения:
- Проверьте синтаксис 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
# ⚠️ Осторожно: это удалит ВСЕ неиспользуемые данные!
Комментарии
Войдите, чтобы оставить комментарий
Пока нет комментариев. Будьте первым!