Содержание ТЗ
09. Развёртывание и эксплуатация
Развёртывание статики и полного стека, nginx, Docker, CI/CD, бэкапы и runbook.
В документе нет паролей и ключей. Доступы передаются через защищённый канал и хранятся в секрет-менеджере. Пароль сервера из переписки в репозиторий не добавляется.
1. Фактическое состояние сервера (на 18.10.2026)
| Параметр | Значение |
|---|---|
| IP | 134.0.112.10 |
| ОС | Ubuntu 26.04 LTS |
| nginx | 1.28.3 (host, sites-available/enabled) |
| Docker | 29.1.3, Compose v2 |
| RAM | 3,8 ГБ (занято ~2,4 ГБ, доступно ~1,4 ГБ) |
| Диск | 58 ГБ (занято 28 ГБ, свободно 30 ГБ) |
| Swap | 3 ГБ (занято 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 (примерно) |
Правила «не сломать чужое»:
- Не изменять существующие файлы в
/etc/nginx/sites-available/*иsites-enabled/*. - Не перезапускать чужие контейнеры, не трогать их compose-файлы и volumes.
- Не занимать занятые порты; новые порты — только после
ss -ltn. - Не менять глобальный
nginx.confбез необходимости. - Перед
nginx -tиsystemctl reload nginx— проверка конфигурации. - Все новые файлы — с префиксом
easy-connect. - Отдельная Docker-сеть и отдельные volume.
- Не удалять и не перезаписывать
/var/wwwдругих проектов. - Бэкап конфигурации nginx перед изменениями.
- Изменения — пошагово, с проверкой после каждого шага.
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 владельцем домена:
- Убедиться, что A-записи указывают на
134.0.112.10(dig +short easy-connect.ru). - Выпустить сертификат:
certbot certonly --webroot -w /var/www/letsencrypt -d easy-connect.ru -d www.easy-connect.ru. - Включить 443-блоки и редирект с 80.
- Проверить 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: сайт недоступен
curl -I http://127.0.0.1/с Host.nginx -t,systemctl status nginx.- Проверить симлинк
/var/www/easy-connect/current. - Проверить права (
www-dataчитает). - Логи:
/var/log/nginx/error.log,access.log. - Откатить релиз/конфиг при необходимости.
5.3. Runbook: сертификат не обновляется
certbot certificates.certbot renew --dry-run.- Проверить DNS и доступность
/.well-known/acme-challenge/. - Проверить, что порт 80 не занят другим server-блоком.
- Выпустить вручную, перезагрузить nginx.
5.4. Runbook: рост очереди
- Проверить
queue_depthи ошибки воркеров. - Проверить лимиты провайдера/429.
- Увеличить concurrency воркеров (при наличии ресурсов).
- Включить circuit breaker для проблемного коннектора.
- Уведомить пользователей, опубликовать инцидент.
5.5. Runbook: утечка токенов
- Отозвать ключи шифрования/ротировать.
- Принудительно разлогинить сессии.
- Пометить подключения
needs_reauth. - Уведомить пользователей.
- Расследование, отчёт, меры.
6. Чек-лист первого развёртывания (статика)
- Сборка
out/готова, проверена локально. - На сервере созданы каталоги, права корректны.
-
nginx -tдо и после. - Сделан бэкап
sites-available. - Новый конфиг не пересекается по
server_nameи портам. - Симлинк включён, nginx перезагружен.
- Проверка по IP/Host и preview-домену.
- Сертификат preview выпущен, HTTPS работает.
-
certbot renew --dry-runуспешен. - Существующие сайты проверены (curl каждого домена).
- Настроен бэкап и мониторинг.
- Инструкция по смене DNS передана владельцу домена.
7. Ограничения текущего этапа
- На сервере размещены статический сайт и backend MVP (API + PostgreSQL) с лимитами ресурсов.
- Полный production-стек (web SSR, worker/BullMQ, Redis, MinIO) требует отдельного сервера или апгрейда.
- Домен
easy-connect.ruпока указывает на старый лендинг; переключение DNS — за владельцем домена. - ЮKassa и реальные OAuth-коннекторы не подключены; на preview включён
DEMO_MODE=true. - Пароль сервера, полученный в переписке, рекомендуется сменить и перейти на SSH-ключи с отключением парольного входа.