diff --git a/.env.example b/.env.example index 0731897..03c7397 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,15 @@ # Lendry ID — переменные для docker compose # Скопируйте в .env или запустите: ./install.sh -# Режим: local (порты) | production (Nginx + SSL) +# Режим: local | intranet | production INSTALL_MODE=local +# Nginx: auto (Windows → Docker) | host (Linux) | docker +NGINX_MODE=auto + +# SSL: none (HTTP) | selfsigned (HTTPS локально) | letsencrypt (интернет) +SSL_TYPE=none + # Домены без протокола (install.sh подставит PUBLIC_* URL) DOMAIN_API= DOMAIN_FRONTEND= @@ -19,7 +25,7 @@ PUBLIC_FRONTEND_URL=http://localhost:3002 PUBLIC_DOCS_URL=http://localhost:3003 PUBLIC_WS_URL=ws://localhost:8085/ws -# Nginx + Let's Encrypt +# Nginx (USE_NGINX_SSL=true только при SSL_TYPE=letsencrypt|selfsigned) USE_NGINX_SSL=false CERTBOT_EMAIL= diff --git a/.gitignore b/.gitignore index aad8a99..0cdc8bf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ dist !.env.example docker-compose.override.yml .idp-install.json +nginx/certs/* +!nginx/certs/.gitkeep +nginx/conf.d/*.conf coverage generated *.tsbuildinfo diff --git a/apps/docs/lib/docs-pages.ts b/apps/docs/lib/docs-pages.ts index e0aa464..2795f91 100644 --- a/apps/docs/lib/docs-pages.ts +++ b/apps/docs/lib/docs-pages.ts @@ -63,6 +63,21 @@ export const docPages: DocPage[] = [ id: 'quick-start', title: 'Быстрый запуск', blocks: [ + { + type: 'callout', + variant: 'tip', + title: 'Production на сервере', + text: 'Для развёртывания на Linux с Docker, Nginx и SSL используйте скрипт install.sh — подробности в разделе «Развёртывание на сервере».' + }, + { + type: 'code', + language: 'bash', + title: 'Сервер (рекомендуется)', + code: `git clone https://git.lendry.ru/lendry/IdP.git +cd IdP +chmod +x install.sh +./install.sh` + }, { type: 'code', language: 'bash', @@ -158,159 +173,294 @@ curl http://localhost:3003/docs/getting-started` { slug: 'deployment', title: 'Развёртывание на сервере', - description: 'Пошаговый гайд по production-развёртыванию на Linux-сервере с Docker, Nginx и TLS.', + description: 'Автоматическая установка через install.sh: Docker, Nginx, SSL и отдельные домены для каждого сервиса.', sections: [ + { + id: 'install-script', + title: '1. Автоматическая установка (install.sh)', + blocks: [ + { + type: 'paragraph', + text: 'Скрипт install.sh разворачивает весь стек одной командой: Docker, Nginx, генерация .env, reverse proxy. Поддерживает локальную сеть без интернета (--intranet), самоподписанный HTTPS и Let\'s Encrypt для публичных серверов. На Windows Server Nginx запускается в Docker-контейнере.' + }, + { + type: 'callout', + variant: 'tip', + title: 'Локальная сеть (AD, .lpr, .local)', + text: 'Если домены только внутри LAN и Let\'s Encrypt недоступен — используйте ./install.sh --intranet --ssl none. Доступ: http://api.idpmvk.lpr без выхода в интернет.' + }, + { + type: 'code', + language: 'bash', + title: 'Быстрый старт', + code: `git clone https://git.lendry.ru/lendry/IdP.git +cd IdP +chmod +x install.sh +./install.sh` + }, + { + type: 'code', + language: 'bash', + title: 'Production без вопросов', + code: `./install.sh --install --yes \\ + --api-domain api.id.example.ru \\ + --frontend-domain id.example.ru \\ + --docs-domain docs.example.ru \\ + --email admin@example.ru` + }, + { + type: 'code', + language: 'bash', + title: 'Установка без клонирования', + code: `curl -fsSL https://git.lendry.ru/lendry/IdP/raw/branch/main/install.sh | bash` + } + ] + }, + { + id: 'intranet-network', + title: '2. Локальная сеть (без интернета)', + blocks: [ + { + type: 'paragraph', + text: 'Для корпоративной LAN, Windows Server и внутренних доменов (например idpmvk.lpr) не нужен доступ из интернета и Let\'s Encrypt. Скрипт настраивает Nginx на HTTP или HTTPS с самоподписанным сертификатом (openssl). На Windows Server Nginx работает в Docker (профиль proxy).' + }, + { + type: 'code', + language: 'bash', + title: 'Рекомендуемая команда (HTTP)', + code: `./install.sh --intranet --install --yes --offline \\ + --api-domain api.idpmvk.lpr \\ + --frontend-domain id.idpmvk.lpr \\ + --docs-domain docs.idpmvk.lpr \\ + --ssl none` + }, + { + type: 'code', + language: 'bash', + title: 'HTTPS самоподписанный (опционально)', + code: `./install.sh --intranet --install --yes --offline \\ + --api-domain api.idpmvk.lpr \\ + --frontend-domain id.idpmvk.lpr \\ + --docs-domain docs.idpmvk.lpr \\ + --ssl selfsigned` + }, + { + type: 'table', + headers: ['SSL_TYPE', 'Описание', 'Интернет'], + rows: [ + ['none', 'http://api.idpmvk.lpr — без сертификатов', 'Не нужен'], + ['selfsigned', 'https://... — openssl, предупреждение браузера', 'Не нужен'], + ['letsencrypt', 'Публичный HTTPS', 'Нужен'] + ] + }, + { + type: 'callout', + variant: 'warning', + title: 'DNS на клиентах', + text: 'Пропишите A-записи во внутреннем DNS Active Directory или файл hosts на каждом ПК: IP-сервера → api.idpmvk.lpr, id.idpmvk.lpr, docs.idpmvk.lpr. Windows: C:\\Windows\\System32\\drivers\\etc\\hosts' + }, + { + type: 'code', + language: 'text', + title: 'Пример hosts (Windows / Linux)', + code: `192.168.1.10 api.idpmvk.lpr +192.168.1.10 id.idpmvk.lpr +192.168.1.10 docs.idpmvk.lpr` + }, + { + type: 'callout', + variant: 'info', + title: 'Windows Server', + text: 'Запускайте install.sh из Git Bash или WSL. Docker Desktop / Docker Engine должен быть установлен заранее (--offline). Nginx поднимается контейнером на портах 80 и 443.' + } + ] + }, { id: 'server-requirements', - title: '1. Требования к серверу', + title: '3. Требования к серверу', blocks: [ { type: 'list', items: [ - 'Ubuntu 22.04+ / Debian 12+ / аналогичный Linux', + 'Ubuntu 22.04+ / Debian 12+ / Fedora / RHEL (Linux)', 'Минимум 4 GB RAM, 2 vCPU, 40 GB SSD', - 'Домен с DNS A-записью (например id.lendry.ru)', - 'Открытые порты 80 и 443 (остальное — только localhost / internal network)' + 'Отдельные домены или поддомены для API, Frontend и Docs (A-запись → IP сервера)', + 'Открытые порты 80 и 443 (остальные сервисы доступны только через Nginx на localhost)', + 'Права sudo/root для установки Docker и Nginx' ] } ] }, { - id: 'install-docker', - title: '2. Установка Docker', + id: 'domains', + title: '4. Отдельные домены для сервисов', blocks: [ + { + type: 'paragraph', + text: 'При установке и при повторной настройке (--configure-domains) можно указать свой домен для каждого компонента. Текущие значения подставляются по умолчанию — достаточно изменить только нужные.' + }, + { + type: 'table', + headers: ['Сервис', 'Параметр', 'Пример', 'Nginx → порт'], + rows: [ + ['API / Swagger', '--api-domain', 'api.id.example.ru', '127.0.0.1:3000'], + ['Frontend (UI)', '--frontend-domain', 'id.example.ru', '127.0.0.1:3002'], + ['Документация', '--docs-domain', 'docs.example.ru', '127.0.0.1:3003'], + ['WebSocket', '--ws-domain', 'пусто = API + /ws', '127.0.0.1:8085'], + ['MinIO S3', '--minio-domain', 'опционально', '127.0.0.1:9000'], + ['MinIO Console', '--minio-console-domain', 'опционально', '127.0.0.1:9001'] + ] + }, + { + type: 'callout', + variant: 'info', + title: 'WebSocket', + text: 'Если --ws-domain не указан, WebSocket проксируется на том же домене, что и API, по пути /ws (wss://api.example.ru/ws). Для отдельного домена укажите --ws-domain ws.example.ru.' + } + ] + }, + { + id: 'install-menu', + title: '5. Меню управления (повторный запуск)', + blocks: [ + { + type: 'paragraph', + text: 'Если файл .env уже существует, повторный вызов ./install.sh открывает интерактивное меню — как у панелей с автоматическим reverse proxy.' + }, + { + type: 'table', + headers: ['Пункт', 'Действие'], + rows: [ + ['1', 'Полная установка / переустановка стека'], + ['2', 'Настроить или изменить домены, Nginx и SSL'], + ['3', 'Обновить SSL-сертификаты (Let\'s Encrypt)'], + ['4', 'Перезапуск / пересборка контейнеров'], + ['5', 'Статус сервисов и проверка Nginx'], + ['6', 'Удалить конфиги Nginx IdP'], + ['0', 'Выход'] + ] + }, { type: 'code', language: 'bash', - code: `sudo apt update && sudo apt install -y ca-certificates curl gnupg -curl -fsSL https://get.docker.com | sudo sh -sudo usermod -aG docker $USER -docker compose version` + title: 'Смена доменов после установки', + code: `# Интерактивно +./install.sh --configure-domains + +# Или через меню +./install.sh +# → пункт 2 + +# Пример: вынести Swagger на отдельный домен +./install.sh --configure-domains \\ + --api-domain swagger.id.example.ru \\ + --frontend-domain id.example.ru \\ + --docs-domain docs.example.ru` + } + ] + }, + { + id: 'install-commands', + title: '6. Команды и флаги', + blocks: [ + { + type: 'table', + headers: ['Команда / флаг', 'Описание'], + rows: [ + ['./install.sh', 'Интерактивное меню (если .env есть) или первая установка'], + ['./install.sh --install', 'Полная установка'], + ['./install.sh --configure-domains', 'Изменить домены и пересобрать frontend/docs'], + ['./install.sh --renew-ssl', 'Обновить сертификаты Let\'s Encrypt'], + ['./install.sh --restart', 'docker compose up -d --build'], + ['./install.sh --status', 'Статус контейнеров и доменов'], + ['./install.sh --intranet', 'Локальная сеть, HTTP по умолчанию'], + ['./install.sh --ssl none', 'Только HTTP через Nginx'], + ['./install.sh --ssl selfsigned', 'HTTPS самоподписанный'], + ['./install.sh --offline', 'Не скачивать Docker из интернета'], + ['./install.sh --nginx-mode docker', 'Nginx в контейнере (Windows)'], + ['./install.sh --local', 'localhost:3000/3002/3003 без Nginx'], + ['./install.sh --yes', 'Без интерактивных вопросов'], + ['FORCE_ENV=1 ./install.sh', 'Пересоздать .env с новыми секретами'] + ] + }, + { + type: 'code', + language: 'bash', + title: 'Локальная разработка без доменов', + code: `./install.sh --local +# или +./install.sh --install --local --yes` + } + ] + }, + { + id: 'install-automation', + title: '7. Что делает скрипт автоматически', + blocks: [ + { + type: 'list', + items: [ + 'Устанавливает curl, git, openssl, jq (apt/dnf)', + 'Устанавливает Docker и Docker Compose v2 (get.docker.com)', + 'Добавляет пользователя в группу docker', + 'Генерирует .env: пароли PostgreSQL, RabbitMQ, MinIO, JWT-секреты, DATA_ENCRYPTION_KEY', + 'Создаёт docker-compose.override.yml — привязка портов к 127.0.0.1', + 'Устанавливает Nginx и Certbot, настраивает UFW (80, 443)', + 'Генерирует конфиги /etc/nginx/sites-available/lendry-id-*.conf', + 'Получает SSL через certbot (webroot), включает certbot.timer', + 'Запускает docker compose up -d --build и ждёт health сервисов', + 'При смене доменов пересобирает frontend и docs с новыми NEXT_PUBLIC_* URL' + ] + }, + { + type: 'callout', + variant: 'warning', + title: 'Секреты', + text: 'Файл .env не коммитьте в git. Сохраните его в надёжном месте после первой установки. FORCE_ENV=1 генерирует новые секреты и сбрасывает старые.' } ] }, { id: 'clone-config', - title: '3. Клонирование и переменные окружения', + title: '8. Переменные .env (генерируются автоматически)', blocks: [ { type: 'paragraph', - text: 'Создайте файл .env в корне проекта с production-секретами. Никогда не коммитьте его в git.' + text: 'install.sh создаёт и обновляет .env на основе .env.example. При ручном редактировании не меняйте секреты без необходимости — для смены доменов используйте ./install.sh --configure-domains.' }, { type: 'code', language: 'bash', - title: '.env (пример)', - code: `# Секреты JWT и шифрования — сгенерируйте: openssl rand -base64 48 -JWT_ACCESS_SECRET=your-long-access-secret -JWT_REFRESH_SECRET=your-long-refresh-secret -DATA_ENCRYPTION_KEY=your-32-byte-encryption-key-change-me + title: 'Ключевые переменные .env', + code: `# Домены (без протокола) +DOMAIN_API=api.id.example.ru +DOMAIN_FRONTEND=id.example.ru +DOMAIN_DOCS=docs.example.ru +DOMAIN_WS= -# PostgreSQL (можно оставить docker internal) -POSTGRES_USER=lendry -POSTGRES_PASSWORD=strong-db-password -POSTGRES_DB=lendry_id +# Публичные URL (скрипт подставляет https://) +PUBLIC_API_URL=https://api.id.example.ru +PUBLIC_FRONTEND_URL=https://id.example.ru +PUBLIC_DOCS_URL=https://docs.example.ru +PUBLIC_WS_URL=wss://api.id.example.ru/ws -# RabbitMQ -RABBITMQ_DEFAULT_USER=lendry -RABBITMQ_DEFAULT_PASS=strong-rmq-password +USE_NGINX_SSL=true +CERTBOT_EMAIL=admin@example.ru +INSTALL_MODE=production -# MinIO -MINIO_ROOT_USER=minioadmin -MINIO_ROOT_PASSWORD=strong-minio-password` - }, - { - type: 'callout', - variant: 'warning', - title: 'PUBLIC_API_URL', - text: 'В production задайте PUBLIC_API_URL=https://id.lendry.ru для sso-core и api-gateway, а также MINIO_PUBLIC_ENDPOINT=id.lendry.ru (или CDN-домен для MinIO).' - } - ] - }, - { - id: 'docker-compose-prod', - title: '4. Production docker-compose', - blocks: [ - { - type: 'paragraph', - text: 'Для production рекомендуется не публиковать PostgreSQL, Redis и RabbitMQ наружу — оставьте только api-gateway (3000), frontend (3002) и docs (3003) за reverse proxy.' - }, - { - type: 'code', - language: 'bash', - code: `export DOCKER_BUILDKIT=1 -docker compose up -d --build - -# Проверка health всех сервисов -docker compose ps -docker compose logs -f sso-core api-gateway` - } - ] - }, - { - id: 'nginx', - title: '5. Nginx + TLS (Let\'s Encrypt)', - blocks: [ - { - type: 'code', - language: 'nginx', - title: '/etc/nginx/sites-available/id.lendry.ru', - code: `server { - listen 443 ssl http2; - server_name id.lendry.ru; - - ssl_certificate /etc/letsencrypt/live/id.lendry.ru/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/id.lendry.ru/privkey.pem; - - # REST API + Swagger - location / { - proxy_pass http://127.0.0.1:3000; - 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; - } - - # Frontend UI - location /app/ { - proxy_pass http://127.0.0.1:3002/; - proxy_set_header Host $host; - } - - # WebSocket - location /ws { - proxy_pass http://127.0.0.1:8085; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - } -} - -# Документация на docs.id.lendry.ru или /docs-proxy -server { - listen 443 ssl http2; - server_name docs.id.lendry.ru; - ssl_certificate /etc/letsencrypt/live/docs.id.lendry.ru/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/docs.id.lendry.ru/privkey.pem; - - location / { - proxy_pass http://127.0.0.1:3003; - proxy_set_header Host $host; - } -}` - }, - { - type: 'code', - language: 'bash', - code: `sudo certbot --nginx -d id.lendry.ru -d docs.id.lendry.ru -sudo nginx -t && sudo systemctl reload nginx` +# Секреты — генерируются install.sh +JWT_ACCESS_SECRET=... +JWT_REFRESH_SECRET=... +DATA_ENCRYPTION_KEY=... +POSTGRES_PASSWORD=... +RABBITMQ_DEFAULT_PASS=... +MINIO_ROOT_PASSWORD=...` } ] }, { id: 'env-services', - title: '6. Переменные окружения сервисов', + title: '9. Переменные окружения сервисов', blocks: [ { type: 'table', @@ -318,17 +468,50 @@ sudo nginx -t && sudo systemctl reload nginx` rows: [ ['sso-core', 'DATABASE_URL, JWT_*_SECRET, DATA_ENCRYPTION_KEY, REDIS_URL, RABBITMQ_URL, MINIO_*, LDAP_AUTH_URL, PUBLIC_API_URL'], ['api-gateway', 'SSO_CORE_GRPC_URL, JWT_ACCESS_SECRET, PUBLIC_API_URL, MINIO_*'], - ['frontend', 'NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL'], - ['docs', 'NEXT_PUBLIC_API_URL'], + ['frontend', 'NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL (build args + runtime)'], + ['docs', 'NEXT_PUBLIC_API_URL, INTERNAL_API_URL'], ['media-ws', 'REDIS_ADDR, RABBITMQ_URL, JWT_ACCESS_SECRET'], ['ldap-auth', 'PORT=8086'] ] } ] }, + { + id: 'after-install', + title: '10. После установки', + blocks: [ + { + type: 'table', + headers: ['Ресурс', 'URL (production)'], + rows: [ + ['API / Swagger', 'https:///api'], + ['Frontend', 'https://'], + ['Документация', 'https:///docs'], + ['WebSocket', 'wss:///ws или wss:///ws'], + ['MinIO Console', 'только localhost:9001 (если не задан домен)'], + ['RabbitMQ UI', 'только localhost:15672'] + ] + }, + { + type: 'callout', + variant: 'tip', + title: 'Первый пользователь', + text: 'Первый зарегистрированный пользователь автоматически становится супер-администратором. Название проекта (PROJECT_NAME) и LDAP настраиваются в админке: /admin/settings.' + }, + { + type: 'code', + language: 'bash', + title: 'Полезные команды', + code: `docker compose ps +docker compose logs -f api-gateway sso-core +./install.sh --status +./install.sh --renew-ssl` + } + ] + }, { id: 'migrations', - title: '7. Миграции и seed', + title: '11. Миграции и seed', blocks: [ { type: 'code', @@ -341,15 +524,15 @@ docker compose restart sso-core` }, { type: 'callout', - variant: 'tip', + variant: 'info', title: 'SystemSetting', - text: 'Название проекта (PROJECT_NAME), tagline и LDAP-параметры настраиваются в админке: /admin/settings. Frontend и docs подтягивают PROJECT_NAME через GET /settings/public.' + text: 'Название проекта (PROJECT_NAME), tagline и LDAP-параметры настраиваются в админке. Frontend и docs подтягивают PROJECT_NAME через GET /settings/public.' } ] }, { id: 'backup', - title: '8. Резервное копирование', + title: '12. Резервное копирование', blocks: [ { type: 'list', @@ -369,15 +552,16 @@ docker compose restart sso-core` }, { id: 'monitoring', - title: '9. Мониторинг и обновления', + title: '13. Мониторинг и обновления', blocks: [ { type: 'list', items: [ 'Health: GET /health на api-gateway и sso-core', 'Логи: docker compose logs -f --tail=200', - 'Обновление: git pull && docker compose up -d --build', - 'Zero-downtime: blue-green или rolling update через orchestrator (K8s / Swarm)' + 'Обновление кода: git pull && ./install.sh --restart', + 'Смена доменов: ./install.sh --configure-domains', + 'SSL: ./install.sh --renew-ssl или автоматически через certbot.timer' ] } ] diff --git a/docker-compose.yml b/docker-compose.yml index df4a18c..cc3c3eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -245,6 +245,32 @@ services: timeout: 5s retries: 10 + nginx: + profiles: ["proxy"] + image: nginx:1.27-alpine + container_name: lendry-id-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/certs:/etc/nginx/certs:ro + depends_on: + api-gateway: + condition: service_healthy + frontend: + condition: service_healthy + docs: + condition: service_healthy + media-ws: + condition: service_started + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 5s + retries: 3 + volumes: postgres_data: redis_data: diff --git a/install.sh b/install.sh index cb2549c..70f797b 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.0.0" +SCRIPT_VERSION="2.1.0" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -18,6 +18,8 @@ NGINX_PREFIX="lendry-id" NGINX_AVAILABLE="/etc/nginx/sites-available" NGINX_ENABLED="/etc/nginx/sites-enabled" CERTBOT_WEBROOT="/var/www/certbot" +LOCAL_CONF_DIR="${ROOT_DIR}/nginx/conf.d" +LOCAL_CERTS_DIR="${ROOT_DIR}/nginx/certs" COMPOSE_OVERRIDE="${ROOT_DIR}/docker-compose.override.yml" STATE_FILE="${ROOT_DIR}/.idp-install.json" @@ -25,6 +27,9 @@ NON_INTERACTIVE=0 FORCE_ENV="${FORCE_ENV:-0}" ACTION="" INSTALL_LOCAL=0 +INSTALL_INTRANET=0 +OFFLINE="${OFFLINE:-0}" +NGINX_MODE="${NGINX_MODE:-auto}" CLI_DOMAIN_API="" CLI_DOMAIN_FRONTEND="" @@ -33,6 +38,7 @@ CLI_DOMAIN_WS="" CLI_DOMAIN_MINIO="" CLI_DOMAIN_MINIO_CONSOLE="" CLI_EMAIL="" +CLI_SSL_TYPE="" RED='\033[0;31m' GREEN='\033[0;32m' @@ -58,6 +64,12 @@ ${BOLD}Lendry ID v${SCRIPT_VERSION}${NC} — установка и управл ./install.sh --status Статус сервисов ./install.sh --remove-nginx Удалить конфиги Nginx IdP +${BOLD}Режим локальной сети (без интернета / AD-домены .local .lpr):${NC} + --intranet Локальная сеть: HTTP или самоподписанный HTTPS + --ssl TYPE none | selfsigned | letsencrypt (по умолчанию в --intranet: none) + --nginx-mode MODE auto | host | docker (Windows → docker) + --offline Не скачивать Docker из интернета (только если уже установлен) + ${BOLD}Домены (можно указывать отдельно):${NC} --api-domain DOMAIN API / Swagger --frontend-domain DOMAIN Веб-интерфейс @@ -65,23 +77,33 @@ ${BOLD}Домены (можно указывать отдельно):${NC} --ws-domain DOMAIN WebSocket (пусто = API + /ws) --minio-domain DOMAIN MinIO S3 (опционально) --minio-console-domain DOMAIN MinIO Console (опционально) - --email EMAIL Email для Let's Encrypt + --email EMAIL Email для Let's Encrypt (только ssl=letsencrypt) ${BOLD}Прочие опции:${NC} -y, --yes Без интерактивных вопросов - --local Локальная установка (порты, без Nginx) + --local localhost:3000/3002/3003 без Nginx -h, --help Справка ${BOLD}Переменные окружения:${NC} FORCE_ENV=1 Пересоздать .env (секреты будут новые) + OFFLINE=1 То же, что --offline ${BOLD}Примеры:${NC} - ./install.sh + ./install.sh --intranet --install --yes \\ + --api-domain api.idpmvk.lpr \\ + --frontend-domain id.idpmvk.lpr \\ + --docs-domain docs.idpmvk.lpr \\ + --ssl none + + ./install.sh --intranet --ssl selfsigned \\ + --api-domain api.idpmvk.lpr --frontend-domain id.idpmvk.lpr \\ + --docs-domain docs.idpmvk.lpr + ./install.sh --install --yes \\ --api-domain api.id.example.ru \\ --frontend-domain id.example.ru \\ --docs-domain docs.example.ru \\ - --email admin@example.ru + --ssl letsencrypt --email admin@example.ru ./install.sh --configure-domains EOF } @@ -97,6 +119,10 @@ parse_args() { --status) ACTION="status"; shift ;; --remove-nginx) ACTION="remove-nginx"; shift ;; --local) INSTALL_LOCAL=1; shift ;; + --intranet) INSTALL_INTRANET=1; shift ;; + --offline) OFFLINE=1; shift ;; + --ssl) CLI_SSL_TYPE="${2:-}"; shift 2 ;; + --nginx-mode) NGINX_MODE="${2:-auto}"; shift 2 ;; --api-domain) CLI_DOMAIN_API="${2:-}"; shift 2 ;; --frontend-domain) CLI_DOMAIN_FRONTEND="${2:-}"; shift 2 ;; --docs-domain) CLI_DOMAIN_DOCS="${2:-}"; shift 2 ;; @@ -143,8 +169,29 @@ run_root() { } preflight_checks() { - [[ "$(uname -s)" == "Linux" ]] || warn "Скрипт рассчитан на Linux. На других ОС установите Docker вручную." + if [[ "$(uname -s)" != "Linux" ]]; then + warn "ОС не Linux — Nginx будет запущен в Docker (профиль proxy). Убедитесь, что Docker Desktop установлен." + NGINX_MODE="docker" + fi [[ -f docker-compose.yml ]] || fail "Запустите скрипт из корня репозитория (рядом с docker-compose.yml)" + mkdir -p "$LOCAL_CONF_DIR" "$LOCAL_CERTS_DIR" +} + +detect_nginx_mode() { + load_env + if [[ -n "$(env_get NGINX_MODE "")" && "$(env_get NGINX_MODE auto)" != "auto" ]]; then + NGINX_MODE="$(env_get NGINX_MODE auto)" + fi + + if [[ "$NGINX_MODE" == "auto" ]]; then + if [[ "$(uname -s)" == "Linux" ]] && { [[ -d /etc/nginx ]] || has_cmd nginx; }; then + NGINX_MODE="host" + else + NGINX_MODE="docker" + fi + fi + + log "Режим Nginx: ${NGINX_MODE} (host=системный, docker=контейнер)" } install_packages_apt() { @@ -182,7 +229,11 @@ install_docker() { fi fi - log "Установка Docker..." + if [[ "$OFFLINE" -eq 1 || "$INSTALL_INTRANET" -eq 1 ]]; then + fail "Docker не установлен или не запущен. В локальной сети установите Docker вручную (offline-пакет / Docker Desktop) и повторите запуск." + fi + + log "Установка Docker (нужен доступ в интернет)..." curl -fsSL https://get.docker.com | run_root sh run_root systemctl enable docker >/dev/null 2>&1 || true run_root systemctl start docker >/dev/null 2>&1 || true @@ -215,32 +266,50 @@ docker_cmd() { fi } -install_nginx_certbot() { - if has_cmd nginx && has_cmd certbot; then - ok "Nginx и Certbot уже установлены" +install_nginx_host() { + if has_cmd nginx; then + ok "Nginx уже установлен" return fi - log "Установка Nginx и Certbot..." + log "Установка Nginx..." case "$PKG_MANAGER" in - apt) - run_root apt-get install -y -qq nginx certbot python3-certbot-nginx - ;; - dnf) - run_root dnf install -y nginx certbot - ;; - *) - fail "Автоустановка Nginx/Certbot для $OS_ID не поддерживается" - ;; + apt) run_root apt-get install -y -qq nginx ;; + dnf) run_root dnf install -y nginx ;; + *) fail "Автоустановка Nginx для $OS_ID не поддерживается. Установите nginx вручную или используйте --nginx-mode docker" ;; + esac + + run_root systemctl enable nginx >/dev/null 2>&1 || true + ok "Nginx установлен" +} + +install_certbot() { + if has_cmd certbot; then + ok "Certbot уже установлен" + return + fi + + if [[ "$OFFLINE" -eq 1 || "$INSTALL_INTRANET" -eq 1 ]]; then + fail "Certbot недоступен offline. Для локальной сети используйте --ssl none или --ssl selfsigned" + fi + + log "Установка Certbot..." + case "$PKG_MANAGER" in + apt) run_root apt-get install -y -qq certbot python3-certbot-nginx ;; + dnf) run_root dnf install -y certbot ;; + *) fail "Автоустановка Certbot для $OS_ID не поддерживается" ;; esac run_root mkdir -p "$CERTBOT_WEBROOT" - run_root chown -R www-data:www-data "$CERTBOT_WEBROOT" 2>/dev/null \ - || run_root chown -R nginx:nginx "$CERTBOT_WEBROOT" 2>/dev/null \ - || true + ok "Certbot установлен" +} - run_root systemctl enable nginx >/dev/null 2>&1 || true - ok "Nginx и Certbot установлены" +install_nginx_certbot() { + if [[ "$NGINX_MODE" == "docker" ]]; then + ok "Nginx в Docker — установка системного nginx не требуется" + return + fi + install_nginx_host } setup_firewall() { @@ -361,9 +430,9 @@ env_set() { } derive_public_urls() { - local use_ssl="${1:-false}" + local ssl_type="${1:-none}" - if [[ "$INSTALL_LOCAL" -eq 1 || "$use_ssl" != "true" ]]; then + if [[ "$INSTALL_LOCAL" -eq 1 ]]; then if [[ -z "${DOMAIN_API:-}" && -z "${DOMAIN_FRONTEND:-}" ]]; then PUBLIC_API_URL="http://localhost:3000" PUBLIC_FRONTEND_URL="http://localhost:3002" @@ -372,13 +441,14 @@ derive_public_urls() { MINIO_PUBLIC_ENDPOINT="${MINIO_PUBLIC_ENDPOINT:-localhost:9000}" MINIO_USE_SSL="false" USE_NGINX_SSL="false" + SSL_TYPE="none" INSTALL_MODE="local" return fi fi - local scheme="https" - [[ "$use_ssl" == "true" ]] || scheme="http" + local scheme="http" + [[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" ]] && scheme="https" PUBLIC_API_URL="${scheme}://${DOMAIN_API}" PUBLIC_FRONTEND_URL="${scheme}://${DOMAIN_FRONTEND}" @@ -403,9 +473,13 @@ derive_public_urls() { MINIO_USE_SSL=$([[ "$scheme" == "https" ]] && echo true || echo false) fi - USE_NGINX_SSL=$([[ "$use_ssl" == "true" ]] && echo true || echo false) + SSL_TYPE="$ssl_type" + USE_NGINX_SSL=$([[ "$ssl_type" == "none" ]] && echo false || echo true) + if [[ "$INSTALL_LOCAL" -eq 1 ]]; then INSTALL_MODE="local" + elif [[ "$INSTALL_INTRANET" -eq 1 || "$ssl_type" == "none" || "$ssl_type" == "selfsigned" ]]; then + INSTALL_MODE="intranet" elif [[ -n "${DOMAIN_API:-}" ]]; then INSTALL_MODE="production" else @@ -413,8 +487,52 @@ derive_public_urls() { fi } +choose_ssl_type_interactive() { + local default="${1:-none}" + + if [[ -n "$CLI_SSL_TYPE" ]]; then + SSL_TYPE="$CLI_SSL_TYPE" + return + fi + + if [[ "$NON_INTERACTIVE" -eq 1 ]]; then + if [[ "$INSTALL_INTRANET" -eq 1 ]]; then + SSL_TYPE="${default:-none}" + else + SSL_TYPE="${default:-letsencrypt}" + fi + return + fi + + echo "" + echo -e "${BOLD}Тип SSL / доступ:${NC}" + echo " 1) HTTP — локальная сеть, без сертификатов (http://api.idpmvk.lpr)" + echo " 2) HTTPS самоподписанный — локальная сеть, браузер покажет предупреждение" + echo " 3) HTTPS Let's Encrypt — нужен интернет и публичный DNS" + echo "" + + local ssl_choice + if [[ "$INSTALL_INTRANET" -eq 1 ]]; then + read -r -p "Выберите [1-3] [1]: " ssl_choice + ssl_choice="${ssl_choice:-1}" + else + read -r -p "Выберите [1-3] [3]: " ssl_choice + ssl_choice="${ssl_choice:-3}" + fi + + case "$ssl_choice" in + 1) SSL_TYPE="none" ;; + 2) SSL_TYPE="selfsigned" ;; + 3) SSL_TYPE="letsencrypt" ;; + *) SSL_TYPE="$default" ;; + esac +} + configure_domains_interactive() { local existing_api existing_frontend existing_docs existing_ws existing_email + if [[ "$(env_get INSTALL_MODE "")" == "intranet" ]]; then + INSTALL_INTRANET=1 + fi existing_api="$(normalize_domain "$(env_get DOMAIN_API "")")" existing_frontend="$(normalize_domain "$(env_get DOMAIN_FRONTEND "")")" existing_docs="$(normalize_domain "$(env_get DOMAIN_DOCS "")")" @@ -436,19 +554,31 @@ configure_domains_interactive() { DOMAIN_WS="" DOMAIN_MINIO="" DOMAIN_MINIO_CONSOLE="" - derive_public_urls false + derive_public_urls "none" return fi echo "" echo -e "${BOLD}Настройка доменов${NC}" - echo "Укажите отдельный домен для каждого сервиса (A-запись → IP этого сервера)." - echo "Enter — оставить текущее значение. Для локальной установки: ./install.sh --local" + if [[ "$INSTALL_INTRANET" -eq 1 ]]; then + echo "Локальная сеть: укажите внутренние DNS-имена (например idpmvk.lpr)." + echo "Пропишите A-записи во внутреннем DNS или файл hosts на клиентах." + echo "Доступ из интернета и Let's Encrypt не требуются." + else + echo "Укажите домен для каждого сервиса (A-запись → IP сервера)." + fi + echo "Enter — оставить текущее значение. localhost без Nginx: ./install.sh --local" echo "" - if [[ -z "$default_api" ]]; then default_api="api.example.com"; fi - if [[ -z "$default_frontend" ]]; then default_frontend="id.example.com"; fi - if [[ -z "$default_docs" ]]; then default_docs="docs.example.com"; fi + if [[ "$INSTALL_INTRANET" -eq 1 ]]; then + if [[ -z "$default_api" ]]; then default_api="api.idpmvk.lpr"; fi + if [[ -z "$default_frontend" ]]; then default_frontend="id.idpmvk.lpr"; fi + if [[ -z "$default_docs" ]]; then default_docs="docs.idpmvk.lpr"; fi + else + if [[ -z "$default_api" ]]; then default_api="api.example.com"; fi + if [[ -z "$default_frontend" ]]; then default_frontend="id.example.com"; fi + if [[ -z "$default_docs" ]]; then default_docs="docs.example.com"; fi + fi prompt DOMAIN_API "Домен API / Swagger" "$default_api" prompt DOMAIN_FRONTEND "Домен Frontend (UI)" "$default_frontend" @@ -472,20 +602,24 @@ configure_domains_interactive() { [[ -n "$DOMAIN_API" && -n "$DOMAIN_FRONTEND" && -n "$DOMAIN_DOCS" ]] \ || fail "Домены API, Frontend и Docs обязательны (или используйте --local)" - local use_ssl="true" - if [[ "$NON_INTERACTIVE" -ne 1 ]]; then - local ssl_answer - read -r -p "Настроить Nginx + Let's Encrypt SSL? [Y/n]: " ssl_answer - ssl_answer="${ssl_answer:-Y}" - [[ "$ssl_answer" =~ ^[Yy]$ ]] || use_ssl="false" - fi + choose_ssl_type_interactive "$(env_get SSL_TYPE none)" - if [[ "$use_ssl" == "true" ]]; then + case "$SSL_TYPE" in + none | selfsigned | letsencrypt) ;; + *) fail "Некорректный SSL_TYPE: $SSL_TYPE (none | selfsigned | letsencrypt)" ;; + esac + + if [[ "$SSL_TYPE" == "letsencrypt" ]]; then + if [[ "$INSTALL_INTRANET" -eq 1 && "$NON_INTERACTIVE" -ne 1 ]]; then + warn "Let's Encrypt требует доступ из интернета. Для чистой локальной сети выберите HTTP или selfsigned." + fi prompt CERTBOT_EMAIL "Email для Let's Encrypt" "${default_email:-admin@${DOMAIN_FRONTEND}}" - [[ -n "$CERTBOT_EMAIL" ]] || fail "Email обязателен для SSL" + [[ -n "$CERTBOT_EMAIL" ]] || fail "Email обязателен для Let's Encrypt" + else + CERTBOT_EMAIL="" fi - derive_public_urls "$use_ssl" + derive_public_urls "$SSL_TYPE" } write_env_secrets_if_missing() { @@ -518,6 +652,8 @@ persist_domain_env() { write_env_secrets_if_missing env_set INSTALL_MODE "${INSTALL_MODE:-local}" + env_set NGINX_MODE "${NGINX_MODE:-auto}" + env_set SSL_TYPE "${SSL_TYPE:-none}" env_set DOMAIN_API "${DOMAIN_API:-}" env_set DOMAIN_FRONTEND "${DOMAIN_FRONTEND:-}" env_set DOMAIN_DOCS "${DOMAIN_DOCS:-}" @@ -538,9 +674,28 @@ persist_domain_env() { write_compose_override() { load_env - if [[ -n "$(env_get DOMAIN_API "")" && "$(env_get INSTALL_MODE local)" != "local" ]]; then - log "Привязка контейнеров к 127.0.0.1 (доступ через Nginx)..." - cat >"$COMPOSE_OVERRIDE" <<'EOF' + local mode="${NGINX_MODE:-$(env_get NGINX_MODE auto)}" + local has_domains + has_domains="$(env_get DOMAIN_API "")" + + if [[ -n "$has_domains" && "$(env_get INSTALL_MODE local)" != "local" ]]; then + log "Настройка docker-compose override (Nginx: ${mode})..." + if [[ "$mode" == "docker" ]]; then + cat >"$COMPOSE_OVERRIDE" <<'EOF' +services: + api-gateway: + ports: [] + frontend: + ports: [] + docs: + ports: [] + media-ws: + ports: [] + minio: + ports: [] +EOF + else + cat >"$COMPOSE_OVERRIDE" <<'EOF' services: api-gateway: ports: @@ -559,11 +714,104 @@ services: - "127.0.0.1:9000:9000" - "127.0.0.1:9001:9001" EOF + fi else [[ -f "$COMPOSE_OVERRIDE" ]] && rm -f "$COMPOSE_OVERRIDE" fi } +nginx_upstream() { + case "$1" in + api) + [[ "$NGINX_MODE" == "docker" ]] && echo "http://api-gateway:3000" || echo "http://127.0.0.1:3000" + ;; + frontend) + [[ "$NGINX_MODE" == "docker" ]] && echo "http://frontend:3000" || echo "http://127.0.0.1:3002" + ;; + docs) + [[ "$NGINX_MODE" == "docker" ]] && echo "http://docs:3000" || echo "http://127.0.0.1:3003" + ;; + ws) + [[ "$NGINX_MODE" == "docker" ]] && echo "http://media-ws:8085" || echo "http://127.0.0.1:8085" + ;; + minio) + [[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9000" || echo "http://127.0.0.1:9000" + ;; + minio-console) + [[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9001" || echo "http://127.0.0.1:9001" + ;; + esac +} + +nginx_cert_base_path() { + local domain="$1" + local ssl_type="$2" + if [[ "$ssl_type" == "letsencrypt" ]]; then + echo "/etc/letsencrypt/live/${domain}" + elif [[ "$NGINX_MODE" == "docker" ]]; then + echo "/etc/nginx/certs/${domain}" + else + echo "${LOCAL_CERTS_DIR}/${domain}" + fi +} + +generate_self_signed_cert() { + local domain="$1" + local cert_dir="${LOCAL_CERTS_DIR}/${domain}" + + mkdir -p "$cert_dir" + if [[ -f "${cert_dir}/fullchain.pem" && -f "${cert_dir}/privkey.pem" ]]; then + return 0 + fi + + log "Самоподписанный сертификат: ${domain}" + if has_cmd openssl; then + openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ + -keyout "${cert_dir}/privkey.pem" \ + -out "${cert_dir}/fullchain.pem" \ + -subj "/CN=${domain}/O=Lendry ID Intranet/C=RU" \ + -addext "subjectAltName=DNS:${domain}" 2>/dev/null || \ + openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ + -keyout "${cert_dir}/privkey.pem" \ + -out "${cert_dir}/fullchain.pem" \ + -subj "/CN=${domain}/O=Lendry ID Intranet/C=RU" + chmod 600 "${cert_dir}/privkey.pem" 2>/dev/null || true + else + fail "openssl не найден — нужен для самоподписанных сертификатов" + fi +} + +generate_all_self_signed_certs() { + load_env + generate_self_signed_cert "$DOMAIN_API" + generate_self_signed_cert "$DOMAIN_FRONTEND" + generate_self_signed_cert "$DOMAIN_DOCS" + if [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]]; then + generate_self_signed_cert "$DOMAIN_WS" + fi + [[ -n "${DOMAIN_MINIO:-}" ]] && generate_self_signed_cert "$DOMAIN_MINIO" + [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && generate_self_signed_cert "$DOMAIN_MINIO_CONSOLE" + ok "Самоподписанные сертификаты созданы в ${LOCAL_CERTS_DIR}" +} + +nginx_conf_target() { + local label="$1" + if [[ "$NGINX_MODE" == "docker" ]]; then + echo "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-${label}.conf" + else + echo "${NGINX_AVAILABLE}/${NGINX_PREFIX}-${label}.conf" + fi +} + +nginx_publish_config() { + local label="$1" + local conf + conf="$(nginx_conf_target "$label")" + if [[ "$NGINX_MODE" == "host" ]]; then + run_root ln -sf "$conf" "${NGINX_ENABLED}/${NGINX_PREFIX}-${label}.conf" + fi +} + nginx_proxy_headers=" proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; @@ -572,31 +820,42 @@ nginx_proxy_headers=" proxy_set_header Host \$host; proxy_read_timeout 300s; proxy_connect_timeout 75s;" -nginx_ws_location=" location /ws { - proxy_pass http://127.0.0.1:8085/ws; - 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_read_timeout 86400s; - }" - -write_nginx_site_api() { - local domain="$1" - local ssl="${2:-false}" - local ws_on_api="${3:-true}" - local conf="${NGINX_AVAILABLE}/${NGINX_PREFIX}-api.conf" - local ws_block="" - - if [[ "$ws_on_api" == "true" ]]; then - ws_block="$nginx_ws_location" +nginx_write_conf() { + local conf="$1" + if [[ "$NGINX_MODE" == "docker" ]]; then + cat >"$conf" + else + run_root tee "$conf" >/dev/null fi +} - if [[ "$ssl" == "true" ]]; then - run_root tee "$conf" >/dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null || true - run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx || true + rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true + if [[ "$NGINX_MODE" == "host" ]]; then + run_root rm -f \ + "${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf \ + "${NGINX_AVAILABLE}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true + run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx || true + fi ok "Конфиги Nginx IdP удалены" } -write_nginx_http_only() { +write_all_nginx_configs() { load_env - local ssl="false" + local ssl_type="${1:-$(env_get SSL_TYPE none)}" local ws_on_api="true" [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]] && ws_on_api="false" remove_nginx_configs - - write_nginx_site_api "$DOMAIN_API" "$ssl" "$ws_on_api" - write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl" - write_nginx_site_docs "$DOMAIN_DOCS" "$ssl" + write_nginx_site_api "$DOMAIN_API" "$ssl_type" "$ws_on_api" + write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl_type" + write_nginx_site_docs "$DOMAIN_DOCS" "$ssl_type" if [[ "$ws_on_api" == "false" ]]; then - write_nginx_site_ws "$DOMAIN_WS" "$ssl" + write_nginx_site_ws "$DOMAIN_WS" "$ssl_type" else + rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-ws.conf" 2>/dev/null || true run_root rm -f "${NGINX_ENABLED}/${NGINX_PREFIX}-ws.conf" "${NGINX_AVAILABLE}/${NGINX_PREFIX}-ws.conf" 2>/dev/null || true fi - [[ -n "${DOMAIN_MINIO:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO" 9000 "minio" "$ssl" - [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" 9001 "minio-console" "$ssl" + [[ -n "${DOMAIN_MINIO:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO" minio minio "$ssl_type" + [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" minio-console minio-console "$ssl_type" - run_root nginx -t - run_root systemctl reload nginx + if [[ "$NGINX_MODE" == "host" ]]; then + run_root nginx -t + run_root systemctl reload nginx + else + ok "Конфиги Nginx: ${LOCAL_CONF_DIR}" + fi +} + +write_nginx_http_only() { + write_all_nginx_configs "none" } write_nginx_ssl_configs() { load_env - local ssl="true" - local ws_on_api="true" - [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]] && ws_on_api="false" - - remove_nginx_configs - - write_nginx_site_api "$DOMAIN_API" "$ssl" "$ws_on_api" - write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl" - write_nginx_site_docs "$DOMAIN_DOCS" "$ssl" - - if [[ "$ws_on_api" == "false" ]]; then - write_nginx_site_ws "$DOMAIN_WS" "$ssl" - fi - - [[ -n "${DOMAIN_MINIO:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO" 9000 "minio" "$ssl" - [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" 9001 "minio-console" "$ssl" - - run_root nginx -t - run_root systemctl reload nginx + write_all_nginx_configs "$(env_get SSL_TYPE selfsigned)" } obtain_certificate() { @@ -987,7 +1202,7 @@ obtain_certificate() { obtain_all_certificates() { load_env - [[ "$(env_get USE_NGINX_SSL false)" == "true" ]] || return 0 + [[ "$(env_get SSL_TYPE none)" == "letsencrypt" ]] || return 0 local email="${CERTBOT_EMAIL:-}" [[ -n "$email" ]] || fail "CERTBOT_EMAIL не задан" @@ -1006,12 +1221,14 @@ obtain_all_certificates() { if [[ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]]; then run_root mkdir -p /etc/letsencrypt - run_root curl -fsSL https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf \ - -o /etc/letsencrypt/options-ssl-nginx.conf 2>/dev/null || true + if [[ "$OFFLINE" -ne 1 ]]; then + run_root curl -fsSL https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf \ + -o /etc/letsencrypt/options-ssl-nginx.conf 2>/dev/null || true + fi run_root openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null || true fi - write_nginx_ssl_configs + write_all_nginx_configs "letsencrypt" setup_certbot_renewal } @@ -1030,16 +1247,33 @@ setup_nginx_proxy() { [[ -n "${DOMAIN_API:-}" ]] || return 0 [[ "$INSTALL_LOCAL" -eq 1 ]] && return 0 + detect_nginx_mode + env_set NGINX_MODE "$NGINX_MODE" install_nginx_certbot - setup_firewall - if [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then - run_root mkdir -p "$CERTBOT_WEBROOT" - obtain_all_certificates - else - write_nginx_http_only - ok "Nginx настроен (HTTP без SSL)" - fi + local ssl_type + ssl_type="$(env_get SSL_TYPE none)" + + case "$ssl_type" in + none) + write_nginx_http_only + ok "Nginx: HTTP — локальная сеть, интернет не нужен" + ;; + selfsigned) + generate_all_self_signed_certs + write_all_nginx_configs "selfsigned" + ok "Nginx: HTTPS самоподписанный — импортируйте CA в доверенные на клиентах или примите предупреждение браузера" + ;; + letsencrypt) + setup_firewall + install_certbot + run_root mkdir -p "$CERTBOT_WEBROOT" + obtain_all_certificates + ;; + *) + fail "Неизвестный SSL_TYPE: $ssl_type" + ;; + esac } setup_nginx_stack() { @@ -1075,12 +1309,20 @@ deploy_stack() { write_compose_override load_env + local compose_cmd=(compose --env-file .env) + if [[ -n "$(env_get DOMAIN_API "")" && "$(env_get INSTALL_MODE local)" != "local" ]]; then + compose_cmd+=(--profile proxy) + fi + log "Сборка и запуск контейнеров (10–20 минут при первом запуске)..." - docker_cmd compose --env-file .env up -d --build + docker_cmd "${compose_cmd[@]}" up -d --build wait_for_service "api-gateway" 90 wait_for_service "frontend" 60 wait_for_service "docs" 60 + if [[ " ${compose_cmd[*]} " == *" proxy "* ]]; then + wait_for_service "nginx" 30 + fi } save_install_state() { @@ -1091,6 +1333,25 @@ save_install_state() { EOF } +print_intranet_hint() { + load_env + [[ "$(env_get INSTALL_MODE local)" == "intranet" ]] || return 0 + + local server_ip + server_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" + [[ -z "$server_ip" ]] && server_ip="" + + echo -e " ${BOLD}Локальная сеть (DNS / hosts):${NC}" + echo " Пропишите во внутреннем DNS или C:\\Windows\\System32\\drivers\\etc\\hosts:" + echo " ${server_ip} ${DOMAIN_API} ${DOMAIN_FRONTEND} ${DOMAIN_DOCS}" + echo "" + if [[ "$(env_get SSL_TYPE none)" == "selfsigned" ]]; then + echo " Сертификаты: ${LOCAL_CERTS_DIR}/" + echo " Браузер покажет предупреждение — это нормально для локальных доменов." + echo "" + fi +} + print_summary() { load_env @@ -1099,18 +1360,16 @@ print_summary() { echo -e "${GREEN} Lendry ID — развёртывание завершено${NC}" echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}" echo "" + echo -e " ${BOLD}Режим:${NC} ${INSTALL_MODE:-local} | SSL: ${SSL_TYPE:-none} | Nginx: ${NGINX_MODE:-auto}" echo -e " ${BOLD}API / Swagger:${NC} ${PUBLIC_API_URL}/api" echo -e " ${BOLD}Frontend:${NC} ${PUBLIC_FRONTEND_URL}" echo -e " ${BOLD}Документация:${NC} ${PUBLIC_DOCS_URL}/docs" echo -e " ${BOLD}WebSocket:${NC} ${PUBLIC_WS_URL}" - if [[ "$(env_get USE_NGINX_SSL false)" != "true" ]]; then - echo -e " ${BOLD}MinIO Console:${NC} http://localhost:9001" - echo -e " ${BOLD}RabbitMQ UI:${NC} http://localhost:15672" - fi echo "" + print_intranet_hint echo -e " ${BOLD}Управление:${NC} ./install.sh" echo -e " ${BOLD}Смена доменов:${NC} ./install.sh --configure-domains" - echo -e " ${BOLD}Логи:${NC} docker compose logs -f api-gateway sso-core" + echo -e " ${BOLD}Логи:${NC} docker compose logs -f api-gateway nginx" echo "" echo " Первый зарегистрированный пользователь станет супер-администратором." echo " Секреты в .env — не коммитьте в git." @@ -1127,6 +1386,8 @@ show_status() { echo -e "${BOLD}Статус Lendry ID${NC}" echo "" echo " INSTALL_MODE: $(env_get INSTALL_MODE local)" + echo " SSL_TYPE: $(env_get SSL_TYPE none)" + echo " NGINX_MODE: $(env_get NGINX_MODE auto)" echo " USE_NGINX_SSL: $(env_get USE_NGINX_SSL false)" echo " DOMAIN_API: $(env_get DOMAIN_API -)" echo " DOMAIN_FRONTEND: $(env_get DOMAIN_FRONTEND -)" @@ -1175,17 +1436,35 @@ action_configure_domains() { action_renew_ssl() { load_env - [[ "$(env_get USE_NGINX_SSL false)" == "true" ]] || fail "SSL не используется (USE_NGINX_SSL=false)" - install_nginx_certbot - run_root certbot renew --quiet - write_nginx_ssl_configs - ok "SSL-сертификаты обновлены" + local ssl_type + ssl_type="$(env_get SSL_TYPE none)" + + case "$ssl_type" in + letsencrypt) + install_certbot + run_root certbot renew --quiet + write_all_nginx_configs "letsencrypt" + ok "Let's Encrypt сертификаты обновлены" + ;; + selfsigned) + generate_all_self_signed_certs + write_all_nginx_configs "selfsigned" + ok "Самоподписанные сертификаты пересозданы" + ;; + none) + fail "SSL отключён (SSL_TYPE=none). Выберите --ssl selfsigned или letsencrypt." + ;; + esac } action_restart() { load_env write_compose_override - docker_cmd compose --env-file .env up -d --build + local compose_cmd=(compose --env-file .env) + if [[ -n "$(env_get DOMAIN_API "")" && "$(env_get INSTALL_MODE local)" != "local" ]]; then + compose_cmd+=(--profile proxy) + fi + docker_cmd "${compose_cmd[@]}" up -d --build ok "Контейнеры перезапущены" show_status } @@ -1200,7 +1479,7 @@ show_menu() { echo "" echo " 1) Полная установка / переустановка стека" echo " 2) Настроить / изменить домены и Nginx + SSL" - echo " 3) Обновить SSL-сертификаты (Let's Encrypt)" + echo " 3) Обновить SSL-сертификаты (selfsigned / Let's Encrypt)" echo " 4) Перезапуск / пересборка контейнеров" echo " 5) Статус сервисов" echo " 6) Удалить конфиги Nginx IdP" diff --git a/nginx/certs/.gitkeep b/nginx/certs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nginx/conf.d/.gitkeep b/nginx/conf.d/.gitkeep new file mode 100644 index 0000000..e69de29