deploy-remote.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. #!/usr/bin/env bash
  2. # ===========================================
  3. # PhotoPlaces — Удалённый деплой на тестовый сервер
  4. # ===========================================
  5. # Запуск на сервере 192.168.88.128:
  6. # curl -fsSL http://192.168.88.96:10880/foxtime/photoplaces/raw/main/deploy/deploy-remote.sh | bash
  7. # Или склонировав репо и запустив:
  8. # cd /home/foxtime/photoplaces && bash deploy/deploy-remote.sh
  9. # ===========================================
  10. # ВНИМАНИЕ: Если репозиторий приватный, нужен токен доступа:
  11. # git clone http://TOKEN@192.168.88.96:10880/foxtime/photoplaces.git
  12. set -euo pipefail
  13. # Цвета для вывода
  14. RED='\033[0;31m'
  15. GREEN='\033[0;32m'
  16. YELLOW='\033[1;33m'
  17. BLUE='\033[0;34m'
  18. NC='\033[0m' # No Color
  19. log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
  20. log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
  21. log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
  22. log_err() { echo -e "${RED}[ERR]${NC} $*"; }
  23. # Конфигурация
  24. REPO_URL="http://192.168.88.96:10880/foxtime/photoplaces.git"
  25. PROJECT_DIR="/home/foxtime/photoplaces"
  26. BRANCH="main"
  27. ENV_FILE="$PROJECT_DIR/deploy/env.prod"
  28. log_info "=== PhotoPlaces Remote Deploy ==="
  29. log_info "Сервер: $(hostname) ($(hostname -I | awk '{print $1}'))"
  30. log_info "Пользователь: $(whoami)"
  31. log_info "Директория: $PROJECT_DIR"
  32. # 1. Проверка зависимостей
  33. check_deps() {
  34. log_info "Проверка зависимостей..."
  35. local missing=()
  36. for cmd in docker git curl openssl; do
  37. if ! command -v "$cmd" >/dev/null 2>&1; then
  38. missing+=("$cmd")
  39. fi
  40. done
  41. if ! docker compose version >/dev/null 2>&1; then
  42. missing+=("docker compose")
  43. fi
  44. if [ ${#missing[@]} -gt 0 ]; then
  45. log_err "Отсутствуют: ${missing[*]}"
  46. log_info "Установите: sudo apt update && sudo apt install -y docker.io docker-compose-plugin git curl openssl"
  47. exit 1
  48. fi
  49. log_ok "Все зависимости установлены"
  50. }
  51. # 2. Клонирование/обновление репозитория
  52. setup_repo() {
  53. log_info "Настройка репозитория..."
  54. if [ -d "$PROJECT_DIR/.git" ]; then
  55. log_info "Репозиторий существует, обновляю..."
  56. cd "$PROJECT_DIR"
  57. git fetch origin
  58. git reset --hard "origin/$BRANCH"
  59. git clean -fd
  60. else
  61. log_info "Клонирую репозиторий..."
  62. if ! git clone --branch "$BRANCH" "$REPO_URL" "$PROJECT_DIR" 2>&1; then
  63. log_err "Не удалось клонировать репозиторий. Возможно, он приватный."
  64. log_info "Создайте токен доступа в Gitea (Settings → Applications → Generate Token)"
  65. log_info "И запустите: git clone http://TOKEN@192.168.88.96:10880/foxtime/photoplaces.git $PROJECT_DIR"
  66. exit 1
  67. fi
  68. cd "$PROJECT_DIR"
  69. fi
  70. log_ok "Репозиторий готов: $(git rev-parse --short HEAD)"
  71. }
  72. # 3. Настройка env.prod
  73. setup_env() {
  74. log_info "Настройка переменных окружения..."
  75. cd "$PROJECT_DIR"
  76. if [ ! -f "$ENV_FILE" ]; then
  77. cp deploy/env.prod.example "$ENV_FILE"
  78. log_info "Создан $ENV_FILE из шаблона"
  79. fi
  80. # Автогенерация секретов если пустые или CHANGE_ME
  81. generate_secret() {
  82. local var="$1"
  83. local current="${!var:-}"
  84. if [[ -z "$current" || "$current" == *"CHANGE_ME"* ]]; then
  85. local secret
  86. secret=$(openssl rand -hex 32)
  87. if [[ "$OSTYPE" == "darwin"* ]]; then
  88. sed -i '' "s/^${var}=.*/${var}=${secret}/" "$ENV_FILE"
  89. else
  90. sed -i "s/^${var}=.*/${var}=${secret}/" "$ENV_FILE"
  91. fi
  92. export "$var=$secret"
  93. log_ok "$var сгенерирован автоматически"
  94. fi
  95. }
  96. # Загружаем текущие значения
  97. set -a
  98. source "$ENV_FILE"
  99. set +a
  100. generate_secret "DB_PASSWORD"
  101. generate_secret "REDIS_PASSWORD"
  102. generate_secret "JWT_SECRET"
  103. generate_secret "JWT_REFRESH_SECRET"
  104. generate_secret "S3_SECRET_KEY"
  105. # Интерактивный ввод YANDEX_MAPS_API_KEY если не задан
  106. prompt_for_yandex_key() {
  107. local current="${YANDEX_MAPS_API_KEY:-}"
  108. if [ -z "$current" ] || [[ "$current" == *"CHANGE_ME"* ]] || [[ "$current" == *"ВАШ_КЛЮЧ"* ]]; then
  109. echo
  110. log_warn "YANDEX_MAPS_API_KEY не задан (нужен для работы карт на фронтенде)"
  111. echo -n "Введите Яндекс.Карты API ключ (или Enter чтобы пропустить): "
  112. read -r yandex_key
  113. if [ -n "$yandex_key" ]; then
  114. if [[ "$OSTYPE" == "darwin"* ]]; then
  115. sed -i '' "s/^YANDEX_MAPS_API_KEY=.*/YANDEX_MAPS_API_KEY=${yandex_key}/" "$ENV_FILE"
  116. else
  117. sed -i "s/^YANDEX_MAPS_API_KEY=.*/YANDEX_MAPS_API_KEY=${yandex_key}/" "$ENV_FILE"
  118. fi
  119. export YANDEX_MAPS_API_KEY="$yandex_key"
  120. log_ok "YANDEX_MAPS_API_KEY сохранён"
  121. else
  122. log_warn "Ключ пропущен — карты не будут работать. Добавьте позже в $ENV_FILE"
  123. fi
  124. fi
  125. }
  126. prompt_for_yandex_key
  127. # Интерактивный ввод CloudPayments (опционально)
  128. prompt_for_cloudpayments() {
  129. local public_id="${CLOUDPAYMENTS_PUBLIC_ID:-}"
  130. local api_secret="${CLOUDPAYMENTS_API_SECRET:-}"
  131. if [ -z "$public_id" ] || [ -z "$api_secret" ]; then
  132. echo
  133. log_info "CloudPayments (опционально, для платежей):"
  134. echo -n "CLOUDPAYMENTS_PUBLIC_ID (Enter чтобы пропустить): "
  135. read -r cp_public
  136. if [ -n "$cp_public" ]; then
  137. if [[ "$OSTYPE" == "darwin"* ]]; then
  138. sed -i '' "s/^CLOUDPAYMENTS_PUBLIC_ID=.*/CLOUDPAYMENTS_PUBLIC_ID=${cp_public}/" "$ENV_FILE"
  139. else
  140. sed -i "s/^CLOUDPAYMENTS_PUBLIC_ID=.*/CLOUDPAYMENTS_PUBLIC_ID=${cp_public}/" "$ENV_FILE"
  141. fi
  142. export CLOUDPAYMENTS_PUBLIC_ID="$cp_public"
  143. fi
  144. echo -n "CLOUDPAYMENTS_API_SECRET (Enter чтобы пропустить): "
  145. read -r cp_secret
  146. if [ -n "$cp_secret" ]; then
  147. if [[ "$OSTYPE" == "darwin"* ]]; then
  148. sed -i '' "s/^CLOUDPAYMENTS_API_SECRET=.*/CLOUDPAYMENTS_API_SECRET=${cp_secret}/" "$ENV_FILE"
  149. else
  150. sed -i "s/^CLOUDPAYMENTS_API_SECRET=.*/CLOUDPAYMENTS_API_SECRET=${cp_secret}/" "$ENV_FILE"
  151. fi
  152. export CLOUDPAYMENTS_API_SECRET="$cp_secret"
  153. fi
  154. fi
  155. }
  156. prompt_for_cloudpayments
  157. # Проверка обязательных переменных
  158. local required=("DB_PASSWORD" "REDIS_PASSWORD" "JWT_SECRET" "JWT_REFRESH_SECRET" "S3_SECRET_KEY" "YANDEX_MAPS_API_KEY")
  159. local missing=()
  160. for var in "${required[@]}"; do
  161. if [ -z "${!var:-}" ]; then
  162. missing+=("$var")
  163. fi
  164. done
  165. if [ ${#missing[@]} -gt 0 ]; then
  166. log_warn "Требуют ручного заполнения в $ENV_FILE:"
  167. for var in "${missing[@]}"; do
  168. echo " - $var"
  169. done
  170. log_info "Откройте $ENV_FILE и заполните недостающие значения, затем запустите скрипт снова"
  171. exit 1
  172. fi
  173. log_ok "Все секреты настроены"
  174. }
  175. # 4. Создание Docker secrets (если Swarm)
  176. create_docker_secrets() {
  177. if docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null | grep -q "active"; then
  178. log_info "Swarm mode: создание Docker secrets..."
  179. printf "%s" "$DB_PASSWORD" | docker secret create db_password - 2>/dev/null || docker secret rm db_password && printf "%s" "$DB_PASSWORD" | docker secret create db_password -
  180. printf "%s" "$REDIS_PASSWORD" | docker secret create redis_password - 2>/dev/null || docker secret rm redis_password && printf "%s" "$REDIS_PASSWORD" | docker secret create redis_password -
  181. printf "%s" "$JWT_SECRET" | docker secret create jwt_secret - 2>/dev/null || docker secret rm jwt_secret && printf "%s" "$JWT_SECRET" | docker secret create jwt_secret -
  182. printf "%s" "$JWT_REFRESH_SECRET" | docker secret create jwt_refresh_secret - 2>/dev/null || docker secret rm jwt_refresh_secret && printf "%s" "$JWT_REFRESH_SECRET" | docker secret create jwt_refresh_secret -
  183. printf "%s" "$S3_SECRET_KEY" | docker secret create s3_secret_key - 2>/dev/null || docker secret rm s3_secret_key && printf "%s" "$S3_SECRET_KEY" | docker secret create s3_secret_key -
  184. log_ok "Docker secrets созданы"
  185. fi
  186. }
  187. # 5. Деплой через docker compose
  188. deploy() {
  189. log_info "Запуск docker compose..."
  190. cd "$PROJECT_DIR"
  191. docker compose -f deploy/docker-compose.prod.yml up -d --build --remove-orphans
  192. log_ok "Контейнеры запущены"
  193. }
  194. # 6. Health checks
  195. health_check() {
  196. log_info "Ожидание готовности сервисов (до 120 сек)..."
  197. local max_attempts=60
  198. local attempt=0
  199. while [ $attempt -lt $max_attempts ]; do
  200. attempt=$((attempt + 1))
  201. # Проверка Caddy (порт 2019 metrics) — curl есть в alpine
  202. if docker compose -f deploy/docker-compose.prod.yml exec -T caddy curl -sf http://localhost:2019/metrics >/dev/null 2>&1; then
  203. # Проверка backend через Caddy (HTTP, SSL на Synology)
  204. if curl -sf "http://localhost/api/v1/health" >/dev/null 2>&1; then
  205. log_ok "Backend здоров"
  206. break
  207. fi
  208. fi
  209. if [ $attempt -eq $max_attempts ]; then
  210. log_err "Таймаут ожидания здоровья"
  211. docker compose -f deploy/docker-compose.prod.yml logs --tail=50 caddy backend
  212. exit 1
  213. fi
  214. sleep 2
  215. done
  216. # Финальная проверка через HTTPS (Synology)
  217. log_info "Проверка через HTTPS (Synology)..."
  218. if curl -sf "https://photoplaces.ru/api/v1/health" >/dev/null 2>&1; then
  219. log_ok "HTTPS health check пройден"
  220. else
  221. log_warn "HTTPS check не прошёл (проверьте настройки Synology Reverse Proxy)"
  222. fi
  223. }
  224. # 7. Вывод результатов
  225. show_result() {
  226. echo
  227. log_ok "=== ДЕПЛОЙ УСПЕШНО ЗАВЕРШЁН ==="
  228. echo
  229. echo " Фронтенд: https://photoplaces.ru"
  230. echo " API: https://api.photoplaces.ru/api/v1"
  231. echo " S3 API: https://api.photoplaces.ru/s3"
  232. echo " MinIO Console: https://api.photoplaces.ru/minio-console/"
  233. echo
  234. echo " Логи: cd $PROJECT_DIR && docker compose -f deploy/docker-compose.prod.yml logs -f"
  235. echo " Статус: cd $PROJECT_DIR && docker compose -f deploy/docker-compose.prod.yml ps"
  236. echo " Остановка: cd $PROJECT_DIR && docker compose -f deploy/docker-compose.prod.yml down"
  237. echo
  238. }
  239. # ===========================================
  240. # MAIN
  241. # ===========================================
  242. main() {
  243. check_deps
  244. setup_repo
  245. setup_env
  246. create_docker_secrets
  247. deploy
  248. health_check
  249. show_result
  250. }
  251. main "$@"