deploy.sh 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. cd "$(dirname "$0")/.."
  4. ENV_FILE="${1:-deploy/env.prod}"
  5. echo "=== PhotoPlaces — деплой ==="
  6. # 1. Load secrets from external source if configured
  7. # Supported: 1password (op), hashicorp vault, aws secretsmanager, or local file
  8. load_secrets() {
  9. if command -v op >/dev/null 2>&1 && [ -n "${OP_VAULT:-}" ] && [ -n "${OP_ITEM:-}" ]; then
  10. echo ">>> Loading secrets from 1Password..."
  11. eval $(op read "op://${OP_VAULT}/${OP_ITEM}" --format=env 2>/dev/null || true)
  12. elif command -v vault >/dev/null 2>&1 && [ -n "${VAULT_ADDR:-}" ] && [ -n "${VAULT_TOKEN:-}" ] && [ -n "${VAULT_PATH:-}" ]; then
  13. echo ">>> Loading secrets from HashiCorp Vault..."
  14. vault kv get -format=json "${VAULT_PATH}" | jq -r '.data.data | to_entries[] | "export \(.key)=\(.value)"' > /tmp/vault_env.sh
  15. source /tmp/vault_env.sh
  16. rm -f /tmp/vault_env.sh
  17. elif command -v aws >/dev/null 2>&1 && [ -n "${AWS_SECRETS_NAME:-}" ]; then
  18. echo ">>> Loading secrets from AWS Secrets Manager..."
  19. aws secretsmanager get-secret-value --secret-id "${AWS_SECRETS_NAME}" --query SecretString --output text | jq -r 'to_entries[] | "export \(.key)=\(.value)"' > /tmp/aws_env.sh
  20. source /tmp/aws_env.sh
  21. rm -f /tmp/aws_env.sh
  22. elif [ -f "$ENV_FILE" ]; then
  23. echo ">>> Loading secrets from $ENV_FILE..."
  24. else
  25. echo "Ошибка: ни один источник секретов не настроен"
  26. echo " - 1Password: export OP_VAULT=... OP_ITEM=..."
  27. echo " - Vault: export VAULT_ADDR=... VAULT_TOKEN=... VAULT_PATH=..."
  28. echo " - AWS: export AWS_SECRETS_NAME=..."
  29. echo " - Local: создайте $ENV_FILE из deploy/env.prod.example"
  30. exit 1
  31. fi
  32. }
  33. load_secrets
  34. # 2. If using local file, check for CHANGE_ME placeholders and offer to generate
  35. if [ -f "$ENV_FILE" ]; then
  36. set -a
  37. source "$ENV_FILE"
  38. set +a
  39. check_secret() {
  40. local var=$1
  41. local val="${!var:-}"
  42. if [[ "$val" == *"CHANGE_ME"* ]] || [[ -z "$val" ]]; then
  43. echo "!!! $var не настроен. Сгенерировать случайный? [y/N]"
  44. read -r answer
  45. if [[ "$answer" == "y" ]]; then
  46. local secret
  47. secret=$(openssl rand -hex 32)
  48. if [[ "$OSTYPE" == "darwin"* ]]; then
  49. sed -i '' "s/^${var}=.*/${var}=${secret}/" "$ENV_FILE"
  50. else
  51. sed -i "s/^${var}=.*/${var}=${secret}/" "$ENV_FILE"
  52. fi
  53. echo "✓ $var сгенерирован"
  54. export "$var=$secret"
  55. else
  56. echo "Пропущено. Не забудьте настроить вручную!"
  57. fi
  58. fi
  59. }
  60. check_secret "DB_PASSWORD"
  61. check_secret "REDIS_PASSWORD"
  62. check_secret "JWT_SECRET"
  63. check_secret "JWT_REFRESH_SECRET"
  64. check_secret "S3_SECRET_KEY"
  65. fi
  66. # 3. Validate required secrets
  67. required_vars=("DB_PASSWORD" "REDIS_PASSWORD" "JWT_SECRET" "JWT_REFRESH_SECRET" "S3_SECRET_KEY" "S3_PUBLIC_ENDPOINT" "NEXT_PUBLIC_API_URL" "ALLOWED_ORIGINS" "DOMAIN")
  68. missing=()
  69. for var in "${required_vars[@]}"; do
  70. if [ -z "${!var:-}" ]; then
  71. missing+=("$var")
  72. fi
  73. done
  74. if [ ${#missing[@]} -gt 0 ]; then
  75. echo "Ошибка: отсутствуют обязательные переменные:"
  76. printf ' %s\n' "${missing[@]}"
  77. exit 1
  78. fi
  79. # 4. Create Docker secrets if in Swarm mode
  80. create_docker_secrets() {
  81. if docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null | grep -q "active"; then
  82. echo ">>> Swarm mode detected, creating Docker secrets..."
  83. 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 -
  84. 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 -
  85. 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 -
  86. 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 -
  87. 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 -
  88. fi
  89. }
  90. create_docker_secrets
  91. # 5. Build and deploy
  92. echo ">>> Собираю и запускаю контейнеры..."
  93. docker compose -f deploy/docker-compose.prod.yml up -d --build
  94. # 6. Health check (Caddy -> backend -> migrations)
  95. # SSL на Synology, Caddy работает на HTTP (порт 80)
  96. echo ">>> Проверяю здоровье (ждём Caddy + миграции + старт)..."
  97. for i in $(seq 1 60); do
  98. # Check Caddy health (port 2019 metrics endpoint)
  99. if docker compose -f deploy/docker-compose.prod.yml exec -T caddy wget -q --spider http://localhost:2019/metrics 2>/dev/null; then
  100. # Caddy is up, check backend through Caddy (HTTP, SSL на Synology)
  101. HEALTH=$(curl -sf "http://localhost/api/v1/health" 2>/dev/null || curl -sf http://localhost:8080/api/v1/health 2>/dev/null || echo '{"status":"fail"}')
  102. if echo "$HEALTH" | grep -q '"ok"'; then
  103. break
  104. fi
  105. fi
  106. if [ $i -eq 60 ]; then
  107. echo "✗ Таймаут ожидания здоровья API"
  108. docker compose -f deploy/docker-compose.prod.yml logs --tail=30 caddy backend
  109. exit 1
  110. fi
  111. sleep 2
  112. done
  113. echo "✓ Деплой успешен!"
  114. echo "Фронтенд: https://${DOMAIN}"
  115. echo "API: https://api.${DOMAIN}/api/v1"
  116. echo "S3 API: https://api.${DOMAIN}/s3 (presigned URLs)"
  117. echo "MinIO консоль: только внутри Docker сети (SSH туннель при необходимости)"