#!/usr/bin/env bash # =========================================== # PhotoPlaces — Удалённый деплой на тестовый сервер # =========================================== # Запуск на сервере 192.168.88.128: # curl -fsSL http://192.168.88.96:10880/foxtime/photoplaces/raw/main/deploy/deploy-remote.sh | bash # Или склонировав репо и запустив: # cd /home/foxtime/photoplaces && bash deploy/deploy-remote.sh # =========================================== # ВНИМАНИЕ: Если репозиторий приватный, нужен токен доступа: # git clone http://TOKEN@192.168.88.96:10880/foxtime/photoplaces.git set -euo pipefail # Цвета для вывода RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_err() { echo -e "${RED}[ERR]${NC} $*"; } # Конфигурация REPO_URL="http://192.168.88.96:10880/foxtime/photoplaces.git" PROJECT_DIR="/home/foxtime/photoplaces" BRANCH="main" ENV_FILE="$PROJECT_DIR/deploy/env.prod" log_info "=== PhotoPlaces Remote Deploy ===" log_info "Сервер: $(hostname) ($(hostname -I | awk '{print $1}'))" log_info "Пользователь: $(whoami)" log_info "Директория: $PROJECT_DIR" # 1. Проверка зависимостей и прав доступа check_deps() { log_info "Проверка зависимостей..." local missing=() for cmd in docker git curl openssl; do if ! command -v "$cmd" >/dev/null 2>&1; then missing+=("$cmd") fi done if ! docker compose version >/dev/null 2>&1; then missing+=("docker compose") fi if [ ${#missing[@]} -gt 0 ]; then log_err "Отсутствуют: ${missing[*]}" log_info "Установите: sudo apt update && sudo apt install -y docker.io docker-compose-plugin git curl openssl" exit 1 fi # Проверка прав доступа к docker if ! docker info >/dev/null 2>&1; then log_warn "Нет доступа к docker daemon. Пытаюсь добавить пользователя в группу docker..." if sudo usermod -aG docker "$(whoami)" 2>/dev/null; then log_ok "Пользователь добавлен в группу docker. Перезапустите скрипт или выполните: newgrp docker" log_warn "Выполните: newgrp docker && bash $0" exit 0 else log_err "Не удалось добавить в группу docker. Запустите от sudo или настройте права вручную." exit 1 fi fi log_ok "Все зависимости установлены, права доступа к docker OK" } # 2. Клонирование/обновление репозитория setup_repo() { log_info "Настройка репозитория..." if [ -d "$PROJECT_DIR/.git" ]; then log_info "Репозиторий существует, обновляю..." cd "$PROJECT_DIR" git fetch origin git reset --hard "origin/$BRANCH" git clean -fd else log_info "Клонирую репозиторий..." if ! git clone --branch "$BRANCH" "$REPO_URL" "$PROJECT_DIR" 2>&1; then log_err "Не удалось клонировать репозиторий. Возможно, он приватный." log_info "Создайте токен доступа в Gitea (Settings → Applications → Generate Token)" log_info "И запустите: git clone http://TOKEN@192.168.88.96:10880/foxtime/photoplaces.git $PROJECT_DIR" exit 1 fi cd "$PROJECT_DIR" fi log_ok "Репозиторий готов: $(git rev-parse --short HEAD)" } # 3. Настройка env.prod setup_env() { log_info "Настройка переменных окружения..." cd "$PROJECT_DIR" if [ ! -f "$ENV_FILE" ]; then cp deploy/env.prod.example "$ENV_FILE" log_info "Создан $ENV_FILE из шаблона" fi # Автогенерация секретов если пустые или CHANGE_ME generate_secret() { local var="$1" local current="${!var:-}" if [[ -z "$current" || "$current" == *"CHANGE_ME"* ]]; then local secret secret=$(openssl rand -hex 32) if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s/^${var}=.*/${var}=${secret}/" "$ENV_FILE" else sed -i "s/^${var}=.*/${var}=${secret}/" "$ENV_FILE" fi export "$var=$secret" log_ok "$var сгенерирован автоматически" fi } # Загружаем текущие значения set -a source "$ENV_FILE" set +a generate_secret "DB_PASSWORD" generate_secret "REDIS_PASSWORD" generate_secret "JWT_SECRET" generate_secret "JWT_REFRESH_SECRET" generate_secret "S3_SECRET_KEY" # Хелпер для чтения из терминала (работает и при curl | bash) read_from_tty() { local prompt="$1" local var_name="$2" if [ -t 0 ]; then # stdin — терминал echo -n "$prompt" read -r "$var_name" elif [ -e /dev/tty ]; then # stdin — пайп, но есть /dev/tty echo -n "$prompt" > /dev/tty read -r "$var_name" < /dev/tty else # Нет доступа к терминалу log_warn "Нет интерактивного терминала, пропуск ввода $var_name" eval "$var_name=\"\"" fi } # Интерактивный ввод YANDEX_MAPS_API_KEY если не задан prompt_for_yandex_key() { local current="${YANDEX_MAPS_API_KEY:-}" if [ -z "$current" ] || [[ "$current" == *"CHANGE_ME"* ]] || [[ "$current" == *"ВАШ_КЛЮЧ"* ]]; then echo log_warn "YANDEX_MAPS_API_KEY не задан (нужен для работы карт на фронтенде)" read_from_tty "Введите Яндекс.Карты API ключ (или Enter чтобы пропустить): " yandex_key if [ -n "$yandex_key" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s/^YANDEX_MAPS_API_KEY=.*/YANDEX_MAPS_API_KEY=${yandex_key}/" "$ENV_FILE" else sed -i "s/^YANDEX_MAPS_API_KEY=.*/YANDEX_MAPS_API_KEY=${yandex_key}/" "$ENV_FILE" fi export YANDEX_MAPS_API_KEY="$yandex_key" log_ok "YANDEX_MAPS_API_KEY сохранён" else log_warn "Ключ пропущен — карты не будут работать. Добавьте позже в $ENV_FILE" fi fi } prompt_for_yandex_key # Интерактивный ввод CloudPayments (опционально) prompt_for_cloudpayments() { local public_id="${CLOUDPAYMENTS_PUBLIC_ID:-}" local api_secret="${CLOUDPAYMENTS_API_SECRET:-}" if [ -z "$public_id" ] || [ -z "$api_secret" ]; then echo log_info "CloudPayments (опционально, для платежей):" read_from_tty "CLOUDPAYMENTS_PUBLIC_ID (Enter чтобы пропустить): " cp_public if [ -n "$cp_public" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s/^CLOUDPAYMENTS_PUBLIC_ID=.*/CLOUDPAYMENTS_PUBLIC_ID=${cp_public}/" "$ENV_FILE" else sed -i "s/^CLOUDPAYMENTS_PUBLIC_ID=.*/CLOUDPAYMENTS_PUBLIC_ID=${cp_public}/" "$ENV_FILE" fi export CLOUDPAYMENTS_PUBLIC_ID="$cp_public" fi read_from_tty "CLOUDPAYMENTS_API_SECRET (Enter чтобы пропустить): " cp_secret if [ -n "$cp_secret" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s/^CLOUDPAYMENTS_API_SECRET=.*/CLOUDPAYMENTS_API_SECRET=${cp_secret}/" "$ENV_FILE" else sed -i "s/^CLOUDPAYMENTS_API_SECRET=.*/CLOUDPAYMENTS_API_SECRET=${cp_secret}/" "$ENV_FILE" fi export CLOUDPAYMENTS_API_SECRET="$cp_secret" fi fi } prompt_for_cloudpayments # Интерактивный ввод DOMAIN если не задан prompt_for_domain() { local current="${DOMAIN:-}" if [ -z "$current" ] || [[ "$current" == *"CHANGE_ME"* ]] || [[ "$current" == *"photoplaces.ru"* && "$current" != "photoplaces.ru" ]]; then echo log_info "DOMAIN (основной домен, например photoplaces.ru):" read_from_tty "DOMAIN (Enter для photoplaces.ru): " domain_input if [ -n "$domain_input" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s/^DOMAIN=.*/DOMAIN=${domain_input}/" "$ENV_FILE" else sed -i "s/^DOMAIN=.*/DOMAIN=${domain_input}/" "$ENV_FILE" fi export DOMAIN="$domain_input" log_ok "DOMAIN сохранён: $domain_input" else # default if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s/^DOMAIN=.*/DOMAIN=photoplaces.ru/" "$ENV_FILE" else sed -i "s/^DOMAIN=.*/DOMAIN=photoplaces.ru/" "$ENV_FILE" fi export DOMAIN="photoplaces.ru" log_ok "DOMAIN установлен по умолчанию: photoplaces.ru" fi fi } prompt_for_domain # Проверка обязательных переменных local required=("DB_PASSWORD" "REDIS_PASSWORD" "JWT_SECRET" "JWT_REFRESH_SECRET" "S3_SECRET_KEY" "YANDEX_MAPS_API_KEY" "DOMAIN") local missing=() for var in "${required[@]}"; do if [ -z "${!var:-}" ]; then missing+=("$var") fi done if [ ${#missing[@]} -gt 0 ]; then log_warn "Требуют ручного заполнения в $ENV_FILE:" for var in "${missing[@]}"; do echo " - $var" done log_info "Откройте $ENV_FILE и заполните недостающие значения, затем запустите скрипт снова" exit 1 fi log_ok "Все секреты настроены" } # 4. Docker secrets не используем (только env vars) — функция оставлена для совместимости create_docker_secrets() { log_info "Docker secrets не используются (конфигурация через env vars)" } # 5. Деплой через docker compose deploy() { log_info "Запуск docker compose..." cd "$PROJECT_DIR" # Загружаем переменные окружения для docker compose if [ -f "$ENV_FILE" ]; then docker compose --env-file "$ENV_FILE" -f deploy/docker-compose.prod.yml up -d --build --remove-orphans else docker compose -f deploy/docker-compose.prod.yml up -d --build --remove-orphans fi log_ok "Контейнеры запущены" } # 6. Health checks health_check() { log_info "Ожидание готовности сервисов (до 120 сек)..." local max_attempts=60 local attempt=0 while [ $attempt -lt $max_attempts ]; do attempt=$((attempt + 1)) # Проверка Caddy (порт 2019 metrics) — curl есть в alpine if docker compose -f deploy/docker-compose.prod.yml exec -T caddy curl -sf http://localhost:2019/metrics >/dev/null 2>&1; then # Проверка backend через Caddy (HTTP, SSL на Synology) if curl -sf "http://localhost/api/v1/health" >/dev/null 2>&1; then log_ok "Backend здоров" break fi fi if [ $attempt -eq $max_attempts ]; then log_err "Таймаут ожидания здоровья" docker compose -f deploy/docker-compose.prod.yml logs --tail=50 caddy backend exit 1 fi sleep 2 done # Финальная проверка через HTTPS (Synology) log_info "Проверка через HTTPS (Synology)..." if curl -sf "https://photoplaces.ru/api/v1/health" >/dev/null 2>&1; then log_ok "HTTPS health check пройден" else log_warn "HTTPS check не прошёл (проверьте настройки Synology Reverse Proxy)" fi } # 7. Вывод результатов show_result() { echo log_ok "=== ДЕПЛОЙ УСПЕШНО ЗАВЕРШЁН ===" echo echo " Фронтенд: https://photoplaces.ru" echo " API: https://api.photoplaces.ru/api/v1" echo " S3 API: https://api.photoplaces.ru/s3" echo " MinIO Console: https://api.photoplaces.ru/minio-console/" echo echo " Логи: cd $PROJECT_DIR && docker compose -f deploy/docker-compose.prod.yml logs -f" echo " Статус: cd $PROJECT_DIR && docker compose -f deploy/docker-compose.prod.yml ps" echo " Остановка: cd $PROJECT_DIR && docker compose -f deploy/docker-compose.prod.yml down" echo } # =========================================== # MAIN # =========================================== main() { check_deps setup_repo setup_env create_docker_secrets deploy health_check show_result } main "$@"