Перейти к содержимому
easy-connect
ГлавнаяДокументацияРазвёртывание
Содержание ТЗ
Техническое заданиеВерсия 1.0 · 18.10.2026

09. Развёртывание и эксплуатация

Развёртывание статики и полного стека, nginx, Docker, CI/CD, бэкапы и runbook.

В документе нет паролей и ключей. Доступы передаются через защищённый канал и хранятся в секрет-менеджере. Пароль сервера из переписки в репозиторий не добавляется.


1. Фактическое состояние сервера (на 18.10.2026)

ПараметрЗначение
IP134.0.112.10
ОСUbuntu 26.04 LTS
nginx1.28.3 (host, sites-available/enabled)
Docker29.1.3, Compose v2
RAM3,8 ГБ (занято ~2,4 ГБ, доступно ~1,4 ГБ)
Диск58 ГБ (занято 28 ГБ, свободно 30 ГБ)
Swap3 ГБ (занято 1,5 ГБ)
СертификатыLet's Encrypt для easy-avito.ru, easy-feed.ru, easy-metodist.ru, easy-url.ru, easy-vk.ru
Занятые сайтыeasy-avito, easy-feed, easy-logoped, easy-metodist, easy-url, easy-vk
Занятые порты3000–4400, 8080–8081, 9000–9003 (примерно)

Правила «не сломать чужое»:

  1. Не изменять существующие файлы в /etc/nginx/sites-available/* и sites-enabled/*.
  2. Не перезапускать чужие контейнеры, не трогать их compose-файлы и volumes.
  3. Не занимать занятые порты; новые порты — только после ss -ltn.
  4. Не менять глобальный nginx.conf без необходимости.
  5. Перед nginx -t и systemctl reload nginx — проверка конфигурации.
  6. Все новые файлы — с префиксом easy-connect.
  7. Отдельная Docker-сеть и отдельные volume.
  8. Не удалять и не перезаписывать /var/www других проектов.
  9. Бэкап конфигурации nginx перед изменениями.
  10. Изменения — пошагово, с проверкой после каждого шага.

1.1. DNS

На момент анализа:

  • easy-connect.ru → 176.57.64.41 (Tilda-лендинг).
  • www.easy-connect.ru → 176.57.64.41.
  • NS: ns5.hosting.reg.ru, ns6.hosting.reg.ru.

Целевой сервер — 134.0.112.10. Пока A-запись не изменена, боевой домен на этом сервере не получит валидный сертификат Let's Encrypt. Для проверки используется preview-домен на nip.io, который резолвится в IP сервера автоматически: easy-connect.134.0.112.10.nip.io.

Текущий статус (18.10.2026): статический сайт развёрнут на сервере, preview доступен по HTTPS, сертификат Let's Encrypt выпущен и продлевается автоматически (certbot.timer). Preview закрыт от индексации заголовком X-Robots-Tag: noindex. Боевой домен ожидает смены DNS-записей.

После смены DNS владельцем домена:

  1. Убедиться, что A-записи указывают на 134.0.112.10 (dig +short easy-connect.ru).
  2. Выпустить сертификат: certbot certonly --webroot -w /var/www/letsencrypt -d easy-connect.ru -d www.easy-connect.ru.
  3. Включить 443-блоки и редирект с 80.
  4. Проверить HTTPS, HSTS, редирект www → без www.

2. Этап 1: статический сайт (лендинг + ТЗ)

На текущем сервере размещается только статическая сборка Next.js (output: "export"). Это не требует контейнеров, БД и дополнительной RAM.

2.1. Структура на сервере

/var/www/easy-connect/
  releases/
    20261018-120000/      # сборка (HTML, JS, CSS, docs)
  current -> releases/20261018-120000
/var/www/letsencrypt/     # webroot для ACME
/etc/nginx/sites-available/easy-connect.conf
/etc/nginx/sites-enabled/easy-connect.conf -> ../sites-available/easy-connect.conf
/opt/easy-connect/        # deploy-скрипты и резервные копии конфигов

2.2. Сборка

npm ci
npm run build          # next build → out/

Проверка локально:

npx serve out

2.3. Публикация

STAMP=$(date +%Y%m%d-%H%M%S)
rsync -a --delete out/ /var/www/easy-connect/releases/$STAMP/
ln -sfn /var/www/easy-connect/releases/$STAMP /var/www/easy-connect/current

Атомарная смена симлинка; откат — переключение на предыдущий релиз.

2.4. nginx (этап 1, preview)

# /etc/nginx/sites-available/easy-connect.conf
# Только новые server-блоки. Существующие конфиги не изменяются.

map $http_upgrade $connection_upgrade_ec {
    default upgrade;
    ''      close;
}

# ACME + preview (HTTP)
server {
    listen 80;
    listen [::]:80;
    server_name easy-connect.134.0.112.10.nip.io;

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
    }

    location / {
        root /var/www/easy-connect/current;
        try_files $uri $uri/ $uri.html /index.html;
    }

    location /_next/static/ {
        root /var/www/easy-connect/current;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

# Боевой домен: до смены DNS — только HTTP, после — добавить 443.
server {
    listen 80;
    listen [::]:80;
    server_name easy-connect.ru www.easy-connect.ru;

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
    }

    location / {
        root /var/www/easy-connect/current;
        try_files $uri $uri/ $uri.html /index.html;
    }

    location /_next/static/ {
        root /var/www/easy-connect/current;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

После выпуска сертификата для preview добавить:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name easy-connect.134.0.112.10.nip.io;

    ssl_certificate     /etc/letsencrypt/live/easy-connect.134.0.112.10.nip.io/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/easy-connect.134.0.112.10.nip.io/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    root /var/www/easy-connect/current;
    index index.html;
    try_files $uri $uri/ $uri.html /index.html;

    location /_next/static/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
}

2.5. Включение

# 1. Бэкап текущего состояния
cp -a /etc/nginx/sites-available /opt/easy-connect/nginx-backup-$(date +%Y%m%d-%H%M%S)

# 2. Новый конфиг
# (создать /etc/nginx/sites-available/easy-connect.conf)

# 3. Симлинк
ln -s /etc/nginx/sites-available/easy-connect.conf /etc/nginx/sites-enabled/easy-connect.conf

# 4. Проверка (обязательно!)
nginx -t

# 5. Только при успехе
systemctl reload nginx

# 6. Проверка
curl -I -H "Host: easy-connect.ru" http://127.0.0.1/
curl -I https://easy-connect.134.0.112.10.nip.io/

Если nginx -t не прошёл: удалить симлинк, не перезагружать nginx, разобрать ошибку. Существующие сайты не затрагиваются.

2.6. Сертификат preview

mkdir -p /var/www/letsencrypt
certbot certonly --webroot -w /var/www/letsencrypt \
  -d easy-connect.134.0.112.10.nip.io \
  --non-interactive --agree-tos -m admin@easy-connect.ru
nginx -t && systemctl reload nginx

2.7. Обновление

# локально
npm ci && npm run build
# на сервере
rsync -a --delete out/ /var/www/easy-connect/releases/$STAMP/
ln -sfn /var/www/easy-connect/releases/$STAMP /var/www/easy-connect/current
# очистка старых релизов (оставить 5)
ls -1dt /var/www/easy-connect/releases/* | tail -n +6 | xargs -r rm -rf

2.8. Откат

ln -sfn /var/www/easy-connect/releases/<previous> /var/www/easy-connect/current

2A. Backend MVP на текущем сервере (18.10.2026)

Развёрнут API MVP с ограничением ресурсов. Redis/BullMQ пока не запущены: runtime работает in-process.

2A.1. Размещение

  • /opt/easy-connect/server — исходники, Dockerfile, docker-compose.prod.yml, .env.production (права 600).
  • Compose project easy-connect-server; сеть easy-connect-server_ec_internal.
  • easy-connect-server-api-1 — 127.0.0.1:5101 -> 3000, mem_limit 320m, healthcheck /health/ready.
  • easy-connect-server-postgres-1 — только внутренняя сеть, mem_limit 384m, shared_buffers=64MB.
  • Redis не запущен (не используется); в compose он под профилем queue.
  • Фактическое потребление: ~75 МБ API + ~25 МБ PostgreSQL.

2A.2. Публичный доступ

  • API: https://easy-connect.134.0.112.10.nip.io/api/v1
  • Health: https://easy-connect.134.0.112.10.nip.io/health/ready
  • Кабинет: https://easy-connect.134.0.112.10.nip.io/app/
  • Вход: https://easy-connect.134.0.112.10.nip.io/login/
  • nginx: location /api/ и /health/ → 127.0.0.1:5101, client_max_body_size 1m.
  • DEMO_MODE=true на preview: подключения и действия имитируются, реальные Telegram-сообщения не отправляются.
  • Для реального Telegram: DEMO_MODE=false, токен бота и chat_id. Остальные коннекторы — следующий этап.
  • Cookies: Secure, SameSite=Lax; API и сайт на одном origin.

2A.3. Обновление

# локально: архив server/ без node_modules, dist, .env, .data
tar -czf /tmp/ec-server-src.tar.gz -C server \
  --exclude=node_modules --exclude=dist --exclude='.env*' \
  --exclude=.data --exclude=coverage --exclude=test .

# сервер: аккуратно обновить исходники, не удаляя .env.production
cd /opt/easy-connect/server
docker compose --env-file .env.production -f docker-compose.prod.yml build
docker compose --env-file .env.production -f docker-compose.prod.yml up -d
docker compose --env-file .env.production -f docker-compose.prod.yml ps
curl -fsS http://127.0.0.1:5101/health/ready

Миграции и seed каталога выполняются при старте контейнера. Перед обновлением сделать pg_dump и бэкап nginx. Не выполнять глобальный docker system prune на общем сервере.

2A.4. Бэкапы

  • /opt/easy-connect/backups, ежедневно в 03:30 UTC.
  • easy-connect-backup.timer + easy-connect-backup.service, retention 7 дней.
  • Скрипт: server/scripts/backup.sh.
  • Проверка: systemctl list-timers | grep easy-connect.
  • Восстановление: pg_restore из дампа в контейнер PostgreSQL.

2A.5. Ограничения

  • Runtime in-process: при рестарте API очередь не сохраняется (MVP).
  • Нет ЮKassa, админки, уведомлений и реальных коннекторов кроме Telegram.
  • Общий сервер: следить за free -h, df -h, docker stats. При росте нагрузки вынести backend на отдельный сервер.

3. Этап 2: полный стек (после выделения ресурсов)

Полный production-стек (web SSR, worker/BullMQ, Redis, MinIO) на текущем сервере не запускать. MVP API уже размещён с лимитами — см. 2A.

Требования: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, Ubuntu 24.04/26.04, Docker 29+, отдельный домен/поддомен api.easy-connect.ru.

3.1. Состав

easy-connect-web        Next.js (SSR)     127.0.0.1:5100
easy-connect-api        NestJS            127.0.0.1:5101
easy-connect-worker     BullMQ            внутренняя сеть
easy-connect-postgres   PostgreSQL 16     127.0.0.1:5434
easy-connect-redis      Redis 7           127.0.0.1:6381
easy-connect-minio      MinIO             127.0.0.1:9004/9005

3.2. docker-compose.yml (эскиз)

name: easy-connect

networks:
  ec-net:
    driver: bridge

volumes:
  ec_pgdata:
  ec_redis:
  ec_minio:

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: easyconnect
      POSTGRES_USER: easyconnect
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ec_pgdata:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5434:5432"
    networks: [ec-net]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U easyconnect"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "noeviction"]
    volumes:
      - ec_redis:/data
    ports:
      - "127.0.0.1:6381:6379"
    networks: [ec-net]

  minio:
    image: minio/minio:latest
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - ec_minio:/data
    ports:
      - "127.0.0.1:9004:9000"
      - "127.0.0.1:9005:9001"
    networks: [ec-net]

  api:
    image: ${EC_API_IMAGE}
    restart: unless-stopped
    env_file: .env.production
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    ports:
      - "127.0.0.1:5101:3000"
    networks: [ec-net]
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health/ready"]
      interval: 15s
      timeout: 5s
      retries: 5

  worker:
    image: ${EC_API_IMAGE}
    restart: unless-stopped
    command: ["node", "dist/worker.js"]
    env_file: .env.production
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    networks: [ec-net]

  web:
    image: ${EC_WEB_IMAGE}
    restart: unless-stopped
    env_file: .env.production
    depends_on:
      api:
        condition: service_healthy
    ports:
      - "127.0.0.1:5100:3000"
    networks: [ec-net]

3.3. nginx для полного стека

upstream easyconnect_web { server 127.0.0.1:5100; }
upstream easyconnect_api { server 127.0.0.1:5101; }

server {
    listen 443 ssl;
    http2 on;
    server_name easy-connect.ru;
    # ... ssl ...
    location / {
        proxy_pass http://easyconnect_web;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.easy-connect.ru;
    # ... ssl ...
    client_max_body_size 2m;
    location / {
        proxy_pass http://easyconnect_api;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name hooks.easy-connect.ru;
    # ... ssl ...
    client_max_body_size 1m;
    location / {
        proxy_pass http://easyconnect_api;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 10s;
    }
}

3.4. Переменные окружения (пример, без значений)

NODE_ENV=production
APP_URL=https://easy-connect.ru
API_URL=https://api.easy-connect.ru
HOOKS_URL=https://hooks.easy-connect.ru
DATABASE_URL=postgresql://easyconnect:***@postgres:5432/easyconnect
REDIS_URL=redis://redis:6379
SESSION_SECRET=***
ENCRYPTION_KEY=***
ENCRYPTION_KEY_VERSION=1
YOOKASSA_SHOP_ID=***
YOOKASSA_SECRET_KEY=***
SMTP_URL=***
S3_ENDPOINT=http://minio:9000
S3_BUCKET=easyconnect
S3_ACCESS_KEY=***
S3_SECRET_KEY=***
SENTRY_DSN=***
TELEGRAM_ALERT_CHAT_ID=***

Секреты — только в .env.production с правами 600, владелец easyconnect. Файл в git не попадает.

3.5. Миграции и запуск

docker compose pull
docker compose run --rm api npx prisma migrate deploy
docker compose up -d
docker compose ps
curl -fsS http://127.0.0.1:5101/health/ready

3.6. Обновление без простоя

docker compose pull api worker web
docker compose up -d --no-deps api worker web
# health-check, затем при необходимости откат на предыдущий тег

3.7. Бэкапы

# PostgreSQL
docker exec easy-connect-postgres \
  pg_dump -U easyconnect -Fc easyconnect > /opt/easy-connect/backups/db-$(date +%F).dump

# MinIO/файлы
mc mirror /opt/easy-connect/data /backup/easy-connect/
  • Cron ежедневно 03:30 МСК.
  • Хранение 30 дней, шифрование, выгрузка в отдельное хранилище.
  • Ежемесячная проверка восстановления на staging.

4. CI/CD

4.1. GitHub Actions (этап 1, статика)

name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run build

4.2. Деплой (ручной/по тегу)

  • Сборка артефакта out/.
  • rsync на сервер по SSH-ключу (не паролю).
  • Атомарный симлинк, очистка старых релизов.
  • Smoke: curl -fsS https://.../ + проверка ключевых страниц.
  • Откат при ошибке.

Секреты GitHub: SSH_HOST, SSH_USER, SSH_KEY, DEPLOY_PATH. Пароль в секретах не хранится.


5. Мониторинг и эксплуатация

5.1. Обязательные проверки

  • systemctl status nginx, nginx -t.
  • docker compose ps, healthchecks.
  • Диск, RAM, swap, load average.
  • Очереди, error rate, latency (Grafana).
  • Сертификаты: certbot renew --dry-run раз в месяц.
  • Бэкапы: наличие и размер.

5.2. Runbook: сайт недоступен

  1. curl -I http://127.0.0.1/ с Host.
  2. nginx -t, systemctl status nginx.
  3. Проверить симлинк /var/www/easy-connect/current.
  4. Проверить права (www-data читает).
  5. Логи: /var/log/nginx/error.log, access.log.
  6. Откатить релиз/конфиг при необходимости.

5.3. Runbook: сертификат не обновляется

  1. certbot certificates.
  2. certbot renew --dry-run.
  3. Проверить DNS и доступность /.well-known/acme-challenge/.
  4. Проверить, что порт 80 не занят другим server-блоком.
  5. Выпустить вручную, перезагрузить nginx.

5.4. Runbook: рост очереди

  1. Проверить queue_depth и ошибки воркеров.
  2. Проверить лимиты провайдера/429.
  3. Увеличить concurrency воркеров (при наличии ресурсов).
  4. Включить circuit breaker для проблемного коннектора.
  5. Уведомить пользователей, опубликовать инцидент.

5.5. Runbook: утечка токенов

  1. Отозвать ключи шифрования/ротировать.
  2. Принудительно разлогинить сессии.
  3. Пометить подключения needs_reauth.
  4. Уведомить пользователей.
  5. Расследование, отчёт, меры.

6. Чек-лист первого развёртывания (статика)

  • Сборка out/ готова, проверена локально.
  • На сервере созданы каталоги, права корректны.
  • nginx -t до и после.
  • Сделан бэкап sites-available.
  • Новый конфиг не пересекается по server_name и портам.
  • Симлинк включён, nginx перезагружен.
  • Проверка по IP/Host и preview-домену.
  • Сертификат preview выпущен, HTTPS работает.
  • certbot renew --dry-run успешен.
  • Существующие сайты проверены (curl каждого домена).
  • Настроен бэкап и мониторинг.
  • Инструкция по смене DNS передана владельцу домена.

7. Ограничения текущего этапа

  1. На сервере размещены статический сайт и backend MVP (API + PostgreSQL) с лимитами ресурсов.
  2. Полный production-стек (web SSR, worker/BullMQ, Redis, MinIO) требует отдельного сервера или апгрейда.
  3. Домен easy-connect.ru пока указывает на старый лендинг; переключение DNS — за владельцем домена.
  4. ЮKassa и реальные OAuth-коннекторы не подключены; на preview включён DEMO_MODE=true.
  5. Пароль сервера, полученный в переписке, рекомендуется сменить и перейти на SSH-ключи с отключением парольного входа.