#!/usr/bin/env bash # # ПРОД food-market — деплой ТОЛЬКО по явной команде (--yes). Делает БЭКАП ДО миграций. # НЕ встраивать в ночные задачи: прод обновляется вручную. # # Запуск (с dev-vm): # ~/deploy-prod.sh --yes # сборка+push+деплой # ~/deploy-prod.sh --yes # взять теги из прод .env (не пересобирать) # # Guards: прод-compose указывает на food-market-prod-postgres (не shared-postgres), # .env chmod 600, quick-login выкл, демо-данные выкл, issuer = admin.food-market.kz. # Публичный сайт (public) НЕ трогается. set -uo pipefail PROD_HOST="${PROD_HOST:-nns@192.168.1.190}" PROD_DIR="${PROD_DIR:-~/food-market/deploy}" REGISTRY="${REGISTRY:-192.168.1.193:5001}" REPO="${REPO:-/home/nns/food-market}" API_TAG=""; WEB_TAG=""; YES=0; BUILD=0 for a in "$@"; do case "$a" in --yes) YES=1 ;; --build) BUILD=1 ;; -*) echo "Unknown flag: $a" >&2; exit 2 ;; *) if [[ -z "$API_TAG" ]]; then API_TAG="$a"; elif [[ -z "$WEB_TAG" ]]; then WEB_TAG="$a"; fi ;; esac done if [[ $YES -ne 1 ]]; then echo "ОТКАЗ: прод-деплой требует явного --yes. Это боевой сервер." echo "Usage: $0 [ ] --yes [--build]" exit 2 fi log() { echo "[$(date -Is)] [deploy-prod] $*"; } fail() { echo "[deploy-prod] FAIL: $*" >&2; exit 1; } # ── Guard 1: прод-compose не ссылается на чужой shared-postgres ── REMOTE_COMPOSE="$(ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cat $PROD_DIR/docker-compose.yml" 2>/dev/null)" || fail "не прочитать прод-compose" echo "$REMOTE_COMPOSE" | grep -q 'container_name: food-market-prod-postgres' || fail "в прод-compose нет food-market-prod-postgres" if echo "$REMOTE_COMPOSE" | grep -qE 'container_name:[[:space:]]*shared-postgres|name:[[:space:]]*food-market-postgres-data[[:space:]]*$|127\.0\.0\.1:5434:5432'; then fail "прод-compose ссылается на shared-postgres/чужой том/порт 5434 — СТОП" fi # ── Guard 2: прод .env санитария ── REMOTE_ENV="$(ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cat $PROD_DIR/.env" 2>/dev/null)" || fail "не прочитать прод .env" PERM="$(ssh -o StrictHostKeyChecking=no "$PROD_HOST" "stat -c %a $PROD_DIR/.env" 2>/dev/null)" [[ "$PERM" == "600" ]] || fail "прод .env права $PERM (должно 600)" echo "$REMOTE_ENV" | grep -qiE '^POSTGRES_PASSWORD=food_market_dev$' && fail "прод использует dev-пароль БД — СТОП" echo "$REMOTE_ENV" | grep -qiE '^QUICK_LOGIN_ENABLED=true' && fail "на проде включён quick-login — СТОП" echo "$REMOTE_ENV" | grep -qiE '^SEED_DEMO_DATA=false' || fail "на проде не выставлен SEED_DEMO_DATA=false — СТОП" echo "$REMOTE_ENV" | grep -qiE '^OPENIDDICT_ISSUER=https://admin\.food-market\.kz' || fail "issuer не admin.food-market.kz — СТОП" log "guards OK" # ── (опц.) сборка + push релизных образов ── if [[ $BUILD -eq 1 ]]; then [[ -n "$API_TAG" && -n "$WEB_TAG" ]] || { API_TAG="$(cd "$REPO" && git rev-parse --short HEAD)"; WEB_TAG="$API_TAG"; } log "build+push api=$API_TAG web=$WEB_TAG" ( cd "$REPO" && docker build --build-arg LOCAL_REGISTRY=$REGISTRY --build-arg GIT_SHA="$API_TAG" -f deploy/Dockerfile.api -t $REGISTRY/food-market-api:$API_TAG . ) || fail "build api" ( cd "$REPO" && docker build --build-arg LOCAL_REGISTRY=$REGISTRY -f deploy/Dockerfile.web -t $REGISTRY/food-market-web:$WEB_TAG . ) || fail "build web" docker push $REGISTRY/food-market-api:$API_TAG || fail "push api" docker push $REGISTRY/food-market-web:$WEB_TAG || fail "push web" # обновить теги в прод .env ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cd $PROD_DIR && sed -i 's/^API_TAG=.*/API_TAG=$API_TAG/; s/^WEB_TAG=.*/WEB_TAG=$WEB_TAG/' .env" fi # ── БЭКАП ДО миграций (обязательно) ── log "бэкап БД ДО деплоя…" ssh -o StrictHostKeyChecking=no "$PROD_HOST" "FM_BACKUP_TIER=daily $PROD_DIR/prod-backup.sh" 2>&1 | tail -4 || fail "бэкап не прошёл — деплой остановлен" # ── деплой: pull + up postgres+api+web (НЕ public) ── log "pull + up (миграции применяются при старте api)…" ssh -o StrictHostKeyChecking=no "$PROD_HOST" "cd $PROD_DIR && docker compose pull postgres api web && docker compose up -d postgres api web" 2>&1 | tail -10 || fail "compose up" # ── ждём healthy + smoke ── log "жду api healthy…" OK=0 for i in $(seq 1 30); do if ssh -o StrictHostKeyChecking=no "$PROD_HOST" "curl -fsS http://localhost:8080/health/ready 2>/dev/null | grep -q Healthy"; then OK=1; break; fi sleep 3 done [[ $OK -eq 1 ]] || fail "api не стал Healthy — проверь логи food-market-api" PCODE=$(curl -s -o /dev/null -w '%{http_code}' https://admin.food-market.kz/health/ready 2>/dev/null || echo 000) log "публичный /health/ready → HTTP $PCODE" log "✓ прод-деплой завершён (api=$(ssh $PROD_HOST "grep ^API_TAG= $PROD_DIR/.env | cut -d= -f2"))"