Files
goldenChart/deploy_server_guide.md
T
2026-06-15 13:14:57 +09:00

862 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GoldenChart 서버 배포 가이드 (exdev.co.kr + Jenkins)
> **대상 프로젝트:** `goldenChart` (React Frontend + Spring Boot Backend + MySQL)
> **배포 서버:** `exdev.co.kr`
> **문서 버전:** 1.0 · 최종 갱신: 2026-05-23
Mac Studio HFT 배포 매뉴얼의 **Git Hook(Track A)** · **Jenkins(Track B)** 패턴을 GoldenChart에 맞게 단순화한 문서입니다.
이 프로젝트는 **Docker Compose 3서비스**(mysql / backend / frontend) 중심이며, 별도 PM2·AI Agent 스택은 포함하지 않습니다.
---
## 목차
1. [시스템 개요](#1-시스템-개요)
2. [환경 정보 및 경로](#2-환경-정보-및-경로)
3. [사전 준비 (최초 1회)](#3-사전-준비-최초-1회)
4. [케이스 A: 로컬에서 Git push → 자동 배포](#4-케이스-a-로컬에서-git-push--자동-배포)
5. [케이스 B: deploy-home.sh 수동 배포 (개발용)](#5-케이스-b-deploy-homesh-수동-배포-개발용)
6. [케이스 C: Jenkins 스케줄 자동 배포](#6-케이스-c-jenkins-스케줄-자동-배포)
7. [케이스 D: Jenkins 수동·파라미터 배포](#7-케이스-d-jenkins-수동파라미터-배포)
8. [케이스 E: 서버에서 직접 배포](#8-케이스-e-서버에서-직접-배포)
9. [케이스 F: 서비스별 선택 배포](#9-케이스-f-서비스별-선택-배포)
10. [배포 후 검증](#10-배포-후-검증)
11. [일상 운영 명령어](#11-일상-운영-명령어)
12. [트러블슈팅](#12-트러블슈팅)
13. [부록: deploy.sh · post-receive · Jenkinsfile](#13-부록-deploysh--post-receive--jenkinsfile)
---
## 1. 시스템 개요
GoldenChart 배포는 **공통 스크립트 `deploy.sh`** 를 중심으로 두 가지 트랙을 운영합니다.
```
┌────────────────────────────────────────────────────────────────────┐
│ deploy.sh (서버 공통 진입점) │
│ 예: /Users/aidev/git/deploy.sh (서버에 1회 설치) │
└───────────────┬────────────────────────────┬───────────────────────┘
│ │
Track A (Express) Track B (Scheduled)
git push main → 즉시 배포 Jenkins → 예약/승인 배포
│ │
docker compose build/up docker compose build/up
(frontend / backend 선택 가능) (backend·DB 마이그레 권장)
```
| 트랙 | 트리거 | 적합한 변경 | 다운타임 | 실행 도구 |
|------|--------|------------|---------|----------|
| **Track A** | `git push exdev main` | UI, 소규모 API, 설정 | 짧음 (컨테이너 재기동) | Git `post-receive` |
| **Track B** | Jenkins 크론/수동 | Flyway DB 마이그레, 대규모 백엔드 | 짧음 | Jenkins Freestyle/Pipeline |
| **개발용** | 로컬 `./deploy-home.sh` | 긴급·초기 구축 | `down` 후 재기동 | SSH + tar 동기화 |
### deploy.sh 4단계 (권장)
| Phase | 작업 | 설명 |
|-------|------|------|
| Phase 1 | Git checkout | bare repo → work-tree 동기화 |
| Phase 2 | 빌드 | Docker 이미지 빌드 (`docker compose build`) |
| Phase 3 | 교체 | `docker compose up -d` (서비스별 `--no-deps` 가능) |
| Phase 4 | 검증 | Frontend HTTP + Backend API 응답 확인 |
> **참고:** 저장소에 포함된 [`deploy-home.sh`](./deploy-home.sh) 는 Git 없이 **tar 압축 + SCP** 로 동기화하는 별도 경로입니다. Jenkins·Git Hook 구축 전까지 또는 긴급 배포에 사용합니다.
---
## 2. 환경 정보 및 경로
### 서버 (exdev.co.kr)
| 항목 | 값 (기본) |
|------|-----------|
| 호스트 | `exdev.co.kr` |
| SSH 사용자 | `aidev` |
| SSH 포트 | `22` |
| 프로젝트 work-tree | `/Users/aidev/dev/goldenChart` |
| Git bare 저장소 (권장) | `/Users/aidev/git/goldenChart.git` |
| 배포 스크립트 (권장) | `/Users/aidev/git/deploy.sh` |
| 배포 로그 (권장) | `/Users/aidev/logs/goldenchart/deploy.log` |
> 서버 OS·디렉터리는 환경에 맞게 조정하세요. [`deploy-home.sh`](./deploy-home.sh) 기본값은 work-tree를 `/Users/aidev/dev/goldenChart` 로 사용합니다.
### 로컬 개발 PC
| 항목 | 예시 경로 |
|------|-----------|
| 소스 | `~/dev/goldenChart` |
| Git remote 이름 | `exdev` (권장) |
### 서비스 포트 (docker-compose.yml)
| 서비스 | 컨테이너 | 포트 (기본) | URL |
|--------|---------|------------|-----|
| Frontend (Nginx) | `gc-frontend` | 80, **443** | http://exdev.co.kr/ · https://exdev.co.kr/ |
| Backend (Spring Boot) | `gc-backend` | 8080 | http://exdev.co.kr:8080 (직접) 또는 `/api` (Nginx 경유) |
| MySQL | `gc-mysql` | 3306 | localhost:3306 (서버 내부) |
브라우저에서는 Nginx가 `/api``backend:8080` 으로 프록시합니다.
### Docker Compose 서비스
| 서비스명 | 이미지/빌드 | 역할 |
|---------|------------|------|
| `mysql` | mysql:8.0 | DB + Flyway 마이그레이션 대상 |
| `backend` | `backend/Dockerfile` | Spring Boot API, WebSocket(STOMP) |
| `frontend` | `frontend/Dockerfile` | React 빌드 + Nginx |
### 환경 변수 (.env)
서버 work-tree 루트에 `.env` 파일을 두세요. 예시는 [`.env.example`](./.env.example) 참고.
```bash
MYSQL_ROOT_PASSWORD=... # 운영 환경에서 반드시 변경
MYSQL_DATABASE=stockAnalyzer
MYSQL_USER=stock
MYSQL_PASSWORD=...
BACKEND_PORT=8080
FRONTEND_PORT=80
# backend/docker-compose 환경
GC_SECRETS_ENCRYPTION_KEY=... # 운영용 강한 키로 변경
```
---
## 3. 사전 준비 (최초 1회)
### 3-1. 서버(exdev.co.kr) 인프라 확인
서버에 SSH 접속 후 실행합니다.
```bash
ssh aidev@exdev.co.kr
# Docker
docker --version
docker compose version
docker ps
# 디렉터리 생성
mkdir -p /Users/aidev/git
mkdir -p /Users/aidev/logs/goldenchart
mkdir -p /Users/aidev/dev/goldenChart
```
### 3-2. Git Bare 저장소 생성
```bash
# 서버에서 실행
git init --bare /Users/aidev/git/goldenChart.git
# work-tree (배포 대상) — 이미 clone이 있으면 생략 가능
# 최초 1회: bare에서 checkout
git --git-dir=/Users/aidev/git/goldenChart.git \
--work-tree=/Users/aidev/dev/goldenChart \
checkout -f main 2>/dev/null || true
```
### 3-3. deploy.sh 설치
서버에 공통 배포 스크립트를 둡니다. **부록 13-1**`deploy.sh` 내용을 복사합니다.
```bash
# 로컬에서 서버로 복사 (예시)
scp scripts/deploy.sh aidev@exdev.co.kr:/Users/aidev/git/deploy.sh
# 서버에서
chmod +x /Users/aidev/git/deploy.sh
```
> 저장소에 `scripts/deploy.sh` 가 없으면 [부록 13-1](#13-1-deploysh-서버용) 을 참고해 서버에 직접 생성하세요.
### 3-4. post-receive 훅 등록 (Track A)
```bash
cat > /Users/aidev/git/goldenChart.git/hooks/post-receive << 'HOOK'
#!/bin/bash
set -euo pipefail
while read oldrev newrev refname; do
branch="${refname#refs/heads/}"
if [ "$branch" = "main" ]; then
echo "[post-receive] main push → deploy.sh"
SERVICE_NAME="${SERVICE_NAME:-all}" /Users/aidev/git/deploy.sh
else
echo "[post-receive] $branch — 배포 생략 (main만 자동 배포)"
fi
done
HOOK
chmod +x /Users/aidev/git/goldenChart.git/hooks/post-receive
```
### 3-5. 로컬 PC — Git remote 등록
```bash
cd ~/dev/goldenChart
# SSH remote (권장)
git remote add exdev ssh://aidev@exdev.co.kr/Users/aidev/git/goldenChart.git
# SSH 키 등록 (최초 1회)
ssh-copy-id aidev@exdev.co.kr
# 연결 테스트
git ls-remote exdev
```
### 3-6. 최초 push (저장소·work-tree 초기화)
```bash
cd ~/dev/goldenChart
git checkout main
git push -u exdev main
```
push 완료 시 서버 `post-receive``deploy.sh` 가 자동 실행됩니다.
### 3-7. Jenkins 설치 및 초기 설정 (Track B)
서버에 Jenkins가 없다면 Homebrew + PM2 또는 Docker 중 하나로 설치합니다.
**Homebrew 예시:**
```bash
brew install jenkins-lts openjdk@21
# Jenkins URL: http://127.0.0.1:8080 또는 8090 (설치 방식에 따름)
```
**Jenkins 전역 환경 변수 (필수):**
Jenkins → **Manage Jenkins****System****Global properties****Environment variables**
| 변수명 | 값 (macOS Apple Silicon 예시) |
|--------|------------------------------|
| `PATH` | `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin` |
| `JAVA_HOME` | `/opt/homebrew/opt/openjdk@21` |
**Execute shell** 블록 맨 위에 항상 추가:
```bash
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
```
### 3-8. 서버 .env 및 방화벽
```bash
cd /Users/aidev/dev/goldenChart
cp .env.example .env
# vi .env — 비밀번호·암호화 키 변경
# 외부에서 80/8080 접근 필요 시 (서버 방화벽·라우터 설정은 환경별로 확인)
```
---
## 4. 케이스 A: 로컬에서 Git push → 자동 배포
> **용도:** 개발 완료 후 exdev.co.kr에 반영
> **트리거:** `git push exdev main`
> **특징:** Track A — `main` 브랜치만 자동 배포
### 4-1. 일반 워크플로
```bash
cd ~/dev/goldenChart
git status
git add .
git commit -m "feat: 알림 팝업 레이아웃 개선"
git push exdev main
```
### 4-2. 자동 배포 흐름
```
[로컬] git push exdev main
[exdev] post-receive (main만)
/Users/aidev/git/deploy.sh
Phase 1: git checkout → /Users/aidev/dev/goldenChart
Phase 2: docker compose build
Phase 3: docker compose up -d
Phase 4: HTTP/API 헬스 확인
```
### 4-3. 배포 로그 확인 (서버)
```bash
ssh aidev@exdev.co.kr
tail -f /Users/aidev/logs/goldenchart/deploy.log
```
### 4-4. feature 브랜치 (배포 없음)
```bash
git push exdev feature/my-branch
# → post-receive가 main이 아니면 deploy.sh 실행 안 함
```
### 4-5. Track A에 적합한 변경
| 변경 유형 | Track A | 비고 |
|----------|---------|------|
| `frontend/src/**` | ✅ | `SERVICE_NAME=frontend` 로 더 빠르게 가능 |
| `backend/src/**` (소규모) | ✅ | Flyway 변경 있으면 Track B 권장 |
| `db/migration/V*.sql` 신규 | ⚠️ | Jenkins·장 마감 후 배포 권장 |
| `docker-compose.yml` | ⚠️ | `docker-only` 또는 전체 재배포 |
---
## 5. 케이스 B: deploy-home.sh 수동 배포 (개발용)
> **용도:** Git/Jenkins 미구축 시, 또는 긴급 전체 동기화
> **실행 위치:** 로컬 Mac (`goldenChart` 루트)
```bash
cd ~/dev/goldenChart
# 전체 동기화 + Docker 재빌드 (기본, --no-cache)
./deploy-home.sh
# 프론트엔드만
./deploy-home.sh --frontend
# 백엔드만
./deploy-home.sh --backend
# 캐시 사용 (빠름)
./deploy-home.sh --use-cache
# 상태 / 로그 / 중지
./deploy-home.sh --status
./deploy-home.sh --logs
./deploy-home.sh --down
./deploy-home.sh --shell
```
| 옵션 | 설명 |
|------|------|
| (기본) | tar 동기화 → `docker compose down` → build → up |
| `--no-sync` | 서버 기존 소스로만 재빌드 |
| `--use-cache` | Docker 빌드 캐시 사용 |
| `--frontend` / `--backend` | 해당 서비스만 재빌드 |
환경 변수로 서버 지정:
```bash
REMOTE_HOST=exdev.co.kr REMOTE_USER=aidev ./deploy-home.sh
```
> **주의:** `deploy-home.sh` 는 서버 디렉터리를 **삭제 후 tar 전체 덮어쓰기** 합니다. 서버 전용 `.env`·로컬 수정 사항이 있다면 백업 후 사용하세요.
> 정식 운영은 **Git push + deploy.sh** 를 권장합니다.
---
## 6. 케이스 C: Jenkins 스케줄 자동 배포
> **용도:** Spring Boot + Flyway(DB 스키마) 정기 배포
> **트리거:** Jenkins 크론 또는 Poll SCM
### 6-1. Jenkins Job 생성 (최초 1회)
1. Jenkins UI 접속 (예: `http://127.0.0.1:8090` — 서버에서만 접근 가능할 수 있음)
2. **New Item**`goldenChart-Pipeline`**Freestyle project**
**Source Code Management:**
```
[x] Git
Repository URL : file:///Users/aidev/git/goldenChart.git
Branch : */main
```
**Build Triggers (택 1):**
| 시나리오 | 크론 표현식 |
|---------|-----------|
| 월~금 18:00 (장 마감 후) | `0 18 * * 1-5` |
| 매주 토 02:00 | `0 2 * * 6` |
| 1시간마다 변경 감지 | Poll SCM `H * * * *` |
**Build → Execute shell:**
```bash
#!/bin/bash
set -euo pipefail
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
echo "=== goldenChart Jenkins Build #${BUILD_NUMBER} ==="
echo "시각: $(date)"
SERVICE_NAME=backend /Users/aidev/git/deploy.sh
```
### 6-2. Jenkins에서 Git·Docker 경로 확인
빌드 로그에 다음이 출력되어야 합니다.
```bash
which git && which docker && docker compose version
```
---
## 7. 케이스 D: Jenkins 수동·파라미터 배포
> **용도:** 배포 시점·대상·승인을 UI에서 제어
### 7-1. 파라미터 설정
Job → **General****This project is parameterized**
| 파라미터 | 타입 | 이름 | 선택값 |
|---------|------|------|--------|
| 배포 서비스 | Choice | `SERVICE_NAME` | `all` / `frontend` / `backend` / `docker-only` |
| 배포 유형 | Choice | `DEPLOY_TYPE` | `after_market` / `immediate` |
| 배포 승인 | Boolean | `DEPLOY_APPROVED` | default: false |
**Execute shell:**
```bash
#!/bin/bash
set -euo pipefail
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
if [ "${DEPLOY_APPROVED:-false}" != "true" ]; then
echo "[ERROR] DEPLOY_APPROVED=true 필요"
exit 1
fi
if [ "${DEPLOY_TYPE:-after_market}" = "after_market" ]; then
HOUR=$(date +%H)
if [ "$HOUR" -ge 9 ] && [ "$HOUR" -lt 18 ]; then
echo "[ERROR] 장중(09~18시) after_market 배포 불가"
exit 1
fi
fi
echo "SERVICE_NAME=${SERVICE_NAME:-all}"
SERVICE_NAME="${SERVICE_NAME:-all}" /Users/aidev/git/deploy.sh
```
### 7-2. UI에서 실행
1. `goldenChart-Pipeline`**Build with Parameters**
2. `SERVICE_NAME`, `DEPLOY_TYPE`, `DEPLOY_APPROVED` 설정
3. **Build**
### 7-3. 파라미터 조합 예시
| 상황 | SERVICE_NAME | DEPLOY_TYPE | DEPLOY_APPROVED |
|------|-------------|-------------|-----------------|
| 장 마감 후 백엔드+DB | `backend` | `after_market` | ✅ |
| UI 긴급 수정 | `frontend` | `immediate` | ✅ |
| 컨테이너만 재시작 | `docker-only` | `immediate` | ✅ |
| 전체 배포 | `all` | `after_market` | ✅ |
---
## 8. 케이스 E: 서버에서 직접 배포
> **용도:** Jenkins/Git 없이 서버 터미널에서 즉시 배포
```bash
ssh aidev@exdev.co.kr
# 전체 배포
/Users/aidev/git/deploy.sh
# Git push (서버에서 개발한 경우)
cd /Users/aidev/dev/goldenChart
git add . && git commit -m "fix: ..."
git push exdev main # remote가 서버 bare를 가리킬 때
```
### Docker만 수동 재시작
```bash
cd /Users/aidev/dev/goldenChart
docker compose up -d
docker compose up -d --no-deps backend
docker compose up -d --no-deps frontend
docker compose logs -f gc-backend --tail 100
```
### Backend 수동 빌드 (디버깅)
```bash
cd /Users/aidev/dev/goldenChart
docker compose build backend
docker compose up -d --no-deps backend
```
---
## 9. 케이스 F: 서비스별 선택 배포
`SERVICE_NAME` 환경 변수로 `deploy.sh` 실행 범위를 제한합니다.
| SERVICE_NAME | 빌드 대상 | 배포 |
|-------------|----------|------|
| `frontend` | `frontend` 이미지 | `docker compose up -d --no-deps frontend` |
| `backend` | `backend` 이미지 | `docker compose up -d --no-deps backend` |
| `docker-only` | 스킵 | `docker compose up -d` |
| `all` (기본) | 전체 | `docker compose up -d` |
```bash
SERVICE_NAME=frontend /Users/aidev/git/deploy.sh
SERVICE_NAME=backend /Users/aidev/git/deploy.sh
SERVICE_NAME=docker-only /Users/aidev/git/deploy.sh
```
### 변경 파일 → SERVICE_NAME 가이드
```
frontend/src/**, frontend/nginx.conf → frontend
backend/src/**, backend/pom.xml → backend
db/migration/V*.sql → backend (Track B 권장)
docker-compose.yml, .env → all 또는 docker-only
```
---
## 10. 배포 후 검증
### 10-1. 컨테이너 상태
```bash
cd /Users/aidev/dev/goldenChart
docker compose ps
```
기대: `gc-mysql`, `gc-backend`, `gc-frontend`**running (healthy)**.
### 10-2. Frontend
```bash
curl -s -o /dev/null -w "Frontend HTTP: %{http_code}\n" http://localhost:80/
# 기대: 200
```
브라우저: http://exdev.co.kr/
### 10-3. Backend API
```bash
curl -s -o /dev/null -w "Backend API HTTP: %{http_code}\n" \
http://localhost:8080/api/indicators/list
# 기대: 200
```
### 10-4. MySQL
```bash
docker exec gc-mysql mysqladmin ping -h localhost \
-u stock -p"${MYSQL_PASSWORD:-analyzer}"
```
### 10-5. 배포 로그
```bash
grep -E "배포 완료|배포 실패|ERROR" /Users/aidev/logs/goldenchart/deploy.log | tail -20
```
### 10-6. 로컬에서 원격 확인
```bash
./deploy-home.sh --status
```
---
## 11. 일상 운영 명령어
```bash
cd /Users/aidev/dev/goldenChart
# 시작 / 중지
docker compose up -d
docker compose down
docker compose restart backend
# 로그
docker compose logs -f --tail 100
docker logs -f gc-backend --tail 100
docker logs -f gc-frontend --tail 50
# 이미지 정리 (디스크 부족 시)
docker system prune -f
```
| 하고 싶은 작업 | 명령어 |
|-------------|--------|
| 소스 반영 + 자동 배포 | `git push exdev main` |
| UI만 배포 | `SERVICE_NAME=frontend /Users/aidev/git/deploy.sh` |
| API만 배포 | `SERVICE_NAME=backend /Users/aidev/git/deploy.sh` |
| 긴급 전체 동기화 (로컬) | `./deploy-home.sh` |
| Jenkins 수동 배포 | Build with Parameters |
| 배포 로그 | `tail -f /Users/aidev/logs/goldenchart/deploy.log` |
---
## HTTPS (HTTP + HTTPS 동시 접속)
### exdev.co.kr (운영 — Caddy + gc-frontend)
이 서버는 **443 포트를 ex-chatbot Caddy**가 사용합니다. GoldenChart HTTPS는 Caddy → `host.docker.internal:80` → **gc-frontend** 경로입니다.
| URL | 경로 |
|-----|------|
| `http://exdev.co.kr/` | gc-frontend `:80` 직접 |
| `https://exdev.co.kr/` | Caddy `:443` (TLS) → gc-frontend `:80` |
**주의:** `docker-compose.yml` 에 frontend `443:443` 매핑을 넣으면 Caddy 와 충돌해 **frontend 가 기동되지 않습니다** (HTTP·HTTPS 모두 502). frontend 는 **80만** 노출합니다.
```bash
# frontend 기동 확인
docker ps --filter name=gc-frontend
curl -s -o /dev/null -w "HTTP:%{http_code}\n" http://exdev.co.kr/
curl -sk -o /dev/null -w "HTTPS:%{http_code}\n" https://exdev.co.kr/
```
Caddy TLS 인증서: `exAIChatbot2.0/docker/nginx/certs/` (`obtain-cert-exdev.sh`)
### 단독 서버 (Caddy 없음, nginx 가 직접 TLS)
```bash
docker compose -f docker-compose.yml -f docker-compose.https.yml up -d frontend
./scripts/setup-exdev-https.sh --email you@example.com --domain example.com
```
---
## 12. 트러블슈팅
### 12-1. git push 후 배포가 안 됨
```bash
# 훅 권한
ls -la /Users/aidev/git/goldenChart.git/hooks/post-receive
chmod +x /Users/aidev/git/goldenChart.git/hooks/post-receive
# main 브랜치인지
git branch --show-current
# 훅 수동 테스트
echo "0000000000000000000000000000000000000000 $(git rev-parse HEAD) refs/heads/main" | \
/Users/aidev/git/goldenChart.git/hooks/post-receive
```
### 12-2. deploy.sh / Jenkins — docker 명령 없음
Jenkins **Global PATH** 및 shell 상단:
```bash
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
which docker && docker compose version
```
### 12-3. 배포 락 파일
동시 배포 방지용 (deploy.sh에 구현 시):
```bash
cat /tmp/goldenchart_deploy.lock 2>/dev/null
# 프로세스 없을 때만
rm -f /tmp/goldenchart_deploy.lock
```
### 12-4. Backend 기동 실패 (Flyway / MySQL)
```bash
docker logs gc-backend --tail 150
docker logs gc-mysql --tail 50
docker compose up -d mysql
sleep 20
docker compose up -d --no-deps backend
```
### 12-5. Frontend 502 / API 연결 실패
```bash
docker compose ps
# backend healthy 후 frontend 재시작
docker compose up -d --no-deps backend
docker compose up -d --no-deps frontend
```
### 12-6. deploy-home.sh SSH 실패
```bash
ssh-copy-id -p 22 aidev@exdev.co.kr
ssh aidev@exdev.co.kr "docker info"
./deploy-home.sh --forget-password # 캐시된 비밀번호 초기화
```
### 12-7. 서버 .env가 push마다 삭제됨
`deploy-home.sh` 는 work-tree를 **rm -rf 후 재압축 해제** 합니다.
운영 `.env` 는 서버에만 두고, 로컬 `.env``.gitignore` 처리하세요. Git 기반 `deploy.sh` 는 work-tree를 덮어쓰지 않으므로 `.env` 가 유지됩니다.
---
## PC 데스크톱 클라이언트 (Tauri)
웹 Docker 배포와 **별도 Jenkins Job** `goldenChart-Desktop-Pipeline` 으로 macOS `.dmg` + Windows NSIS `.exe` 를 빌드합니다.
```bash
# 서버 최초 1회
./scripts/install-desktop-build-deps.sh
./scripts/setup-desktop-updater-keys.sh
./scripts/server-install-desktop-jenkins.sh
# 수동 빌드·배포
./scripts/jenkins-desktop-pipeline.sh
```
상세: [docs/desktop-pilot.md](docs/desktop-pilot.md)
---
## 13. 부록: deploy.sh · post-receive · Jenkinsfile
### 13-1. deploy.sh (서버용)
서버 `/Users/aidev/git/deploy.sh` 에 저장합니다. 프로젝트에 추가하려면 `scripts/deploy.sh` 로 복사해 Git에 포함할 수 있습니다.
```bash
#!/bin/bash
# GoldenChart — exdev.co.kr 공통 배포 스크립트
set -euo pipefail
GIT_DIR="${GIT_DIR:-/Users/aidev/git/goldenChart.git}"
WORK_TREE="${WORK_TREE:-/Users/aidev/dev/goldenChart}"
LOG_FILE="${LOG_FILE:-/Users/aidev/logs/goldenchart/deploy.log}"
LOCK_FILE="/tmp/goldenchart_deploy.lock"
SERVICE_NAME="${SERVICE_NAME:-all}"
export PATH="/opt/homebrew/bin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
mkdir -p "$(dirname "$LOG_FILE")"
exec >> "$LOG_FILE" 2>&1
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
if [ -f "$LOCK_FILE" ] && kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then
log "ERROR: 다른 배포 진행 중 (PID $(cat "$LOCK_FILE"))"
exit 1
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
log "========== GoldenChart 배포 시작 (SERVICE_NAME=$SERVICE_NAME) =========="
# Phase 1: checkout
if [ -d "$GIT_DIR" ]; then
log "Phase 1: git checkout -f main → $WORK_TREE"
git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" checkout -f main
else
log "WARN: GIT_DIR 없음 — work-tree 기존 소스 사용"
fi
cd "$WORK_TREE"
# Phase 23: build & up
case "$SERVICE_NAME" in
frontend)
log "Phase 2-3: build & up frontend"
docker compose build frontend
docker compose up -d --no-deps frontend
;;
backend)
log "Phase 2-3: build & up backend"
docker compose build backend
docker compose up -d mysql
docker compose up -d --no-deps backend
;;
docker-only)
log "Phase 2-3: docker compose up (no build)"
docker compose up -d
;;
all|*)
log "Phase 2-3: build & up all"
docker compose build
docker compose up -d
;;
esac
# Phase 4: verify
FRONTEND_PORT="${FRONTEND_PORT:-80}"
BACKEND_PORT="${BACKEND_PORT:-8080}"
HTTP_F=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${FRONTEND_PORT}/" || echo "000")
HTTP_B=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${BACKEND_PORT}/api/indicators/list" || echo "000")
log "검증: Frontend=$HTTP_F Backend=$HTTP_B"
docker compose ps
if [[ "$HTTP_F" =~ ^2 ]] && [[ "$HTTP_B" =~ ^2 ]]; then
log "========== 배포 완료 =========="
else
log "========== 배포 완료 (일부 헬스체크 비정상 — 로그 확인) =========="
fi
```
### 13-2. post-receive (요약)
[3-4절](#3-4-post-receive-훅-등록-track-a) 과 동일. `main` push 시에만 `deploy.sh` 호출.
### 13-3. Jenkinsfile (선택 — Pipeline Job용)
Pipeline Job을 쓸 경우 저장소 루트에 `Jenkinsfile` 추가 예시:
```groovy
pipeline {
agent any
parameters {
choice(name: 'SERVICE_NAME', choices: ['all', 'frontend', 'backend', 'docker-only'], description: '배포 대상')
booleanParam(name: 'DEPLOY_APPROVED', defaultValue: false, description: '배포 승인')
}
stages {
stage('Approve') {
steps {
script {
if (!params.DEPLOY_APPROVED) {
error('DEPLOY_APPROVED=true 필요')
}
}
}
}
stage('Deploy') {
steps {
sh '''
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
SERVICE_NAME=${SERVICE_NAME} /Users/aidev/git/deploy.sh
'''
}
}
}
post {
always {
sh 'tail -30 /Users/aidev/logs/goldenchart/deploy.log || true'
}
}
}
```
### 13-4. HFT 매뉴얼과의 차이
| 항목 | HFT (Mac Studio) | GoldenChart (exdev.co.kr) |
|------|------------------|---------------------------|
| 프로젝트명 | goldenAnalysis | **goldenChart** |
| 배포 스크립트 | `/Volumes/ADATA/git/deploy.sh` | `/Users/aidev/git/deploy.sh` (권장) |
| work-tree | `/Users/aidev/apps/goldenChart` | `/Users/aidev/dev/goldenChart` |
| PM2 / AI Agent | 있음 | **없음** (Docker만) |
| 수동 배포 | — | **`deploy-home.sh`** (tar+SSH) |
| 헬스체크 | Actuator `/actuator/health` | `/api/indicators/list` |
---
*GoldenChart · exdev.co.kr · Docker Compose · 구축 가이드 2026-05-23*