diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..274d8ad --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,34 @@ +pipeline { + agent any + parameters { + choice(name: 'SERVICE_NAME', choices: ['all', 'frontend', 'backend', 'docker-only'], description: '배포 대상') + booleanParam(name: 'DEPLOY_APPROVED', defaultValue: true, description: '배포 승인') + } + stages { + stage('Approve') { + steps { + script { + if (!params.DEPLOY_APPROVED) { + error('DEPLOY_APPROVED=true 필요') + } + } + } + } + stage('Deploy') { + steps { + sh ''' + set -euo pipefail + export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH" + echo "=== BillCare Jenkins Build #${BUILD_NUMBER} ===" + echo "시각: $(date)" + SERVICE_NAME=${SERVICE_NAME} /Volumes/ADATA/git/deploy-billcare.sh + ''' + } + } + } + post { + always { + sh 'tail -40 /Volumes/ADATA/logs/deploy/billcare-deploy.log || true' + } + } +} diff --git a/deploy_server_guide.md b/deploy_server_guide.md new file mode 100644 index 0000000..378435c --- /dev/null +++ b/deploy_server_guide.md @@ -0,0 +1,33 @@ +# BillCare 서버 배포 가이드 (exdev.co.kr + Jenkins) + +## 개요 + +| 트랙 | 트리거 | 동작 | +|------|--------|------| +| Track A | `git push origin main` | `post-receive` → `deploy-billcare.sh` | +| Track B | Jenkins `BillCare-Pipeline` | `deploy-billcare.sh` | + +## 경로 + +| 항목 | 경로 | +|------|------| +| bare repo | `/Volumes/ADATA/git/BillCare.git` | +| work-tree | `/Users/aidev/apps/BillCare` | +| 배포 스크립트 | `/Volumes/ADATA/git/deploy-billcare.sh` | +| 로그 | `/Volumes/ADATA/logs/deploy/billcare-deploy.log` | +| HTTPS | https://billcare.exdev.co.kr | + +## 호스트 포트 + +| 서비스 | 포트 | +|--------|------| +| frontend (Caddy upstream) | **83** | +| backend (직접 헬스체크) | **18082** | +| mysql | **3308** | + +## 로컬 push + +```bash +cd /Users/macbook/dev/billCare +git push origin main +``` diff --git a/docker-compose.yml b/docker-compose.yml index df60401..1cf94bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,14 @@ services: mysql: image: mysql:8.0 container_name: billcare-mysql + restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: billcare MYSQL_USER: billcare MYSQL_PASSWORD: billcare ports: - - "3308:3306" + - "${MYSQL_PORT:-3308}:3306" volumes: - billcare_mysql:/var/lib/mysql command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci @@ -24,6 +25,7 @@ services: context: ./backend dockerfile: Dockerfile container_name: billcare-backend + restart: unless-stopped depends_on: mysql: condition: service_healthy @@ -31,12 +33,12 @@ services: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/billcare?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 SPRING_DATASOURCE_USERNAME: billcare SPRING_DATASOURCE_PASSWORD: billcare - APP_CORS_ORIGINS: http://localhost:82,http://127.0.0.1:82,http://localhost:5174 + APP_CORS_ORIGINS: ${APP_CORS_ORIGINS:-http://localhost:82,http://127.0.0.1:82,http://localhost:5174,https://billcare.exdev.co.kr} APP_UPLOAD_DIR: /app/storage/uploads volumes: - billcare_uploads:/app/storage/uploads ports: - - "8082:8080" + - "${BACKEND_PORT:-8082}:8080" healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/api/health"] interval: 5s @@ -49,11 +51,12 @@ services: context: ./frontend dockerfile: Dockerfile container_name: billcare-frontend + restart: unless-stopped depends_on: backend: condition: service_healthy ports: - - "82:80" + - "${FRONTEND_PORT:-82}:80" volumes: billcare_mysql: diff --git a/scripts/caddy-billcare.Caddyfile b/scripts/caddy-billcare.Caddyfile new file mode 100644 index 0000000..20c3a5d --- /dev/null +++ b/scripts/caddy-billcare.Caddyfile @@ -0,0 +1,25 @@ +# BillCare — exdev Caddy 서브도메인 스니펫 +# host 포트 83 → billcare-frontend +# 실제 적용: ex-chatbot-stack Caddyfile.exdev 에 병합 후 caddy reload + +(billcare_upstream) { + reverse_proxy host.docker.internal:{$BILLCARE_HOST_PORT:83} { + flush_interval -1 + transport http { + read_timeout 120s + write_timeout 120s + versions 1.1 2 + } + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + } +} + +https://{$BILLCARE_DOMAIN:billcare.exdev.co.kr} { + # 공유 cert SAN 에 없을 수 있음 → Caddy 자체 ACME(TLS-ALPN) 사용 + handle { + import billcare_upstream + } +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..df8c73b --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# ============================================================================== +# BillCare 배포 스크립트 — Docker Compose +# 호출: Git post-receive (Track A) | Jenkins (Track B) +# 서버 설치: /Volumes/ADATA/git/deploy-billcare.sh +# ============================================================================== +set -euo pipefail + +GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/BillCare.git}" +WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BillCare}" +BRANCH="${BRANCH:-main}" +DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}" +DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/billcare-deploy.log}" +LOG_TAG="[billcare-deploy]" +SERVICE_NAME="${SERVICE_NAME:-all}" +DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}" +BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://127.0.0.1:18082/api/health}" +FRONTEND_URL="${FRONTEND_URL:-http://127.0.0.1:83/}" + +export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}" +# 서버 포트 (host) — 로컬 개발 82/8082 와 분리, 8082는 kakao-skill 사용 중 +export FRONTEND_PORT="${FRONTEND_PORT:-83}" +export BACKEND_PORT="${BACKEND_PORT:-18082}" +export MYSQL_PORT="${MYSQL_PORT:-3308}" +export APP_CORS_ORIGINS="${APP_CORS_ORIGINS:-https://billcare.exdev.co.kr,http://billcare.exdev.co.kr,http://localhost:83,http://127.0.0.1:83}" + +mkdir -p "$(dirname "$DEPLOY_LOG")" +exec >> "$DEPLOY_LOG" 2>&1 + +log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_TAG $*"; } +info() { log "[INFO] $*"; } +warn() { log "[WARN] $*"; } +err() { log "[ERROR] $*"; exit 1; } + +log "======================================================" +log " BillCare 배포 시작" +log " 서비스 : $SERVICE_NAME" +log " 브랜치 : $BRANCH" +log " 작업트리: $WORK_TREE" +log " compose : $DOCKER_COMPOSE_FILE" +log " 실행자 : $(whoami)" +log "======================================================" + +LOCK_FILE="/tmp/billcare_deploy.lock" +if [ -f "$LOCK_FILE" ]; then + LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "") + if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then + err "다른 배포 진행 중 (PID: $LOCK_PID)" + fi + rm -f "$LOCK_FILE" +fi +echo $$ > "$LOCK_FILE" +trap 'rm -f "$LOCK_FILE"; log "락 파일 해제"' EXIT + +# Phase 1 +info "[Phase 1] Git checkout" +[ ! -d "$WORK_TREE" ] && mkdir -p "$WORK_TREE" +[ ! -d "$GIT_DIR" ] && err "GIT_DIR 없음: $GIT_DIR" +git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH" +COMMIT="$(git --git-dir="$GIT_DIR" rev-parse HEAD)" +info "체크아웃 완료: $(git --git-dir="$GIT_DIR" rev-parse --short HEAD)" + +if [ -d "$WORK_TREE/.git" ]; then + err "work-tree에 중첩 .git 존재 — 제거 후 재배포: rm -rf $WORK_TREE/.git" +fi +WT_HEAD="$(git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" rev-parse HEAD)" +[ "$WT_HEAD" = "$COMMIT" ] || err "work-tree HEAD 불일치: $WT_HEAD != $COMMIT" + +[ ! -f "$DOCKER_COMPOSE_FILE" ] && err "docker-compose.yml 없음: $DOCKER_COMPOSE_FILE" +cd "$WORK_TREE" + +# Phase 2 +info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)" + +_dc_build() { + local svc="$1" + if [ "$DOCKER_NO_CACHE" = "1" ]; then + info "docker compose build --no-cache $svc" + docker compose -f "$DOCKER_COMPOSE_FILE" build --no-cache "$svc" + else + info "docker compose build $svc" + docker compose -f "$DOCKER_COMPOSE_FILE" build "$svc" + fi +} + +case "$SERVICE_NAME" in + frontend) _dc_build frontend ;; + backend) _dc_build backend ;; + all) + _dc_build backend + _dc_build frontend + ;; + docker-only) info "빌드 스킵 (docker-only)" ;; + *) warn "알 수 없는 SERVICE_NAME=$SERVICE_NAME" ;; +esac + +# Phase 3 +info "[Phase 3] Docker Compose up" + +case "$SERVICE_NAME" in + frontend) + docker compose -f "$DOCKER_COMPOSE_FILE" up -d --no-deps frontend + ;; + backend) + docker compose -f "$DOCKER_COMPOSE_FILE" up -d mysql + docker compose -f "$DOCKER_COMPOSE_FILE" up -d --no-deps backend + ;; + all|docker-only) + docker compose -f "$DOCKER_COMPOSE_FILE" up -d + ;; +esac + +# Phase 4 +info "[Phase 4] 헬스체크" + +check_url() { + local name="$1" url="$2" retries="${3:-20}" interval="${4:-5}" + local i code + for i in $(seq 1 "$retries"); do + code=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000") + if [[ "$code" =~ ^2 ]]; then + info "$name OK (HTTP $code)" + return 0 + fi + warn "$name 대기 ($i/$retries) HTTP $code" + sleep "$interval" + done + warn "$name 헬스체크 실패: $url" + return 1 +} + +case "$SERVICE_NAME" in + frontend) check_url "Frontend" "$FRONTEND_URL" || true ;; + backend) check_url "Backend" "$BACKEND_HEALTH_URL" || true ;; + all|docker-only) + check_url "Backend" "$BACKEND_HEALTH_URL" || true + check_url "Frontend" "$FRONTEND_URL" || true + ;; +esac + +log "======================================================" +log " 배포 완료: $(date '+%Y-%m-%d %H:%M:%S')" +log " 커밋: $(git --git-dir="$GIT_DIR" log --oneline -1 2>/dev/null || echo N/A)" +log " URL: https://billcare.exdev.co.kr" +log "======================================================" + +docker compose -f "$DOCKER_COMPOSE_FILE" ps 2>/dev/null || docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" diff --git a/scripts/post-receive b/scripts/post-receive new file mode 100755 index 0000000..a901aea --- /dev/null +++ b/scripts/post-receive @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# BillCare post-receive — Track A (git push main → deploy-billcare.sh) +# 설치: cp scripts/post-receive /Volumes/ADATA/git/BillCare.git/hooks/post-receive && chmod +x ... +set -euo pipefail + +GIT_DIR="/Volumes/ADATA/git/BillCare.git" +WORK_TREE="${WORK_TREE:-/Users/aidev/apps/BillCare}" +DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy-billcare.sh" +SUMMARY_SCRIPT="/Volumes/ADATA/git/git-push-summary.sh" +DEPLOY_LOG="/Volumes/ADATA/logs/deploy/billcare-deploy.log" +SUMMARY_DIR="/Volumes/ADATA/logs/deploy/push-summaries" +HOOK_TAG="[billcare-post-receive]" + +mkdir -p "$(dirname "$DEPLOY_LOG")" "$SUMMARY_DIR" + +echo "" +echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG ==============================" +echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Track A — git push 수신" +echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG ==============================" + +while read -r OLDREV NEWREV REFNAME; do + BRANCH_NAME="${REFNAME#refs/heads/}" + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 브랜치: $BRANCH_NAME ($OLDREV → $NEWREV)" + + if [ -x "$SUMMARY_SCRIPT" ]; then + SUMMARY_FILE="$("$SUMMARY_SCRIPT" "$GIT_DIR" "$OLDREV" "$NEWREV" "$REFNAME" "$SUMMARY_DIR" || true)" + if [ -n "${SUMMARY_FILE:-}" ] && [ -f "$SUMMARY_FILE" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 변경 요약: $SUMMARY_FILE" + head -20 "$SUMMARY_FILE" | sed "s/^/$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG /" + fi + fi + + if [ "$BRANCH_NAME" = "main" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG main → deploy-billcare.sh 실행" + export SERVICE_NAME="${SERVICE_NAME:-all}" + if [ ! -x "$DEPLOY_SCRIPT" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 스크립트 없음: $DEPLOY_SCRIPT" + exit 1 + fi + "$DEPLOY_SCRIPT" 2>&1 | tee -a "$DEPLOY_LOG" + EXIT_CODE=${PIPESTATUS[0]} + if [ "$EXIT_CODE" -ne 0 ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 실패 (exit: $EXIT_CODE)" + exit "$EXIT_CODE" + fi + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 배포 완료 → https://billcare.exdev.co.kr" + else + echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG $BRANCH_NAME — 배포 생략 (main만 자동 배포)" + fi +done