4b86b2a660
- server-dev start/stop/deploy 및 Gitea push 자동 배포 - local-dev 로컬 개발 환경 Co-authored-by: Cursor <cursoragent@cursor.com>
360 lines
11 KiB
Bash
Executable File
360 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 로컬 전체 스택 기동 (exdev AI SSH 터널 + Mongo + RAG + WAS + Skill + chatbotApp)
|
|
#
|
|
# ./local-dev/start-all.sh
|
|
# ./local-dev/start-all.sh --no-tunnel
|
|
# ./local-dev/start-all.sh --skip-java
|
|
# ./local-dev/start-all.sh --no-build
|
|
#
|
|
# 종료: ./local-dev/stop-all.sh
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
LOCAL_DEV_DIR="${ROOT_DIR}/local-dev"
|
|
RUN_DIR="${LOCAL_DEV_DIR}/run"
|
|
LOG_DIR="${LOCAL_DEV_DIR}/logs"
|
|
|
|
RAG_DIR="${ROOT_DIR}/exAiChatBot-chatbot2.0-agent"
|
|
API_DIR="${ROOT_DIR}/chatbotApi-chatbot2.0-agent"
|
|
SKILL_DIR="${ROOT_DIR}/kakaoChatbotSkill-chatbot2.0-agent"
|
|
APP_DIR="${ROOT_DIR}/chatbotApp-chatbot2.0-agent"
|
|
|
|
ENV_TEMPLATE="${LOCAL_DEV_DIR}/env.exdev-ai.template"
|
|
RAG_ENV="${RAG_DIR}/.env"
|
|
COMPOSE_LOCAL="${RAG_DIR}/docker-compose.local.yml"
|
|
|
|
MONGO_CONTAINER="${MONGO_CONTAINER:-ex-chatbot-mongo-local}"
|
|
INTERNAL_TOOL_API_KEY="${INTERNAL_TOOL_API_KEY:-local-dev-key}"
|
|
|
|
LLM_PORT="${LLM_PORT:-16000}"
|
|
EMBED_PORT="${EMBED_PORT:-16001}"
|
|
RERANK_PORT="${RERANK_PORT:-16002}"
|
|
RAG_PORT="${RAG_API_PORT:-28012}"
|
|
ADMIN_PORT="${ADMIN_PORT:-28013}"
|
|
WAS_PORT="${CHATBOT_API_PORT:-8086}"
|
|
SKILL_PORT="${KAKAO_SKILL_PORT:-8083}"
|
|
APP_PORT="${CHATBOT_APP_PORT:-8089}"
|
|
|
|
SKIP_TUNNEL=false
|
|
SKIP_JAVA=false
|
|
NO_BUILD=false
|
|
SYNC_QDRANT=false
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--no-tunnel) SKIP_TUNNEL=true ;;
|
|
--skip-java) SKIP_JAVA=true ;;
|
|
--no-build) NO_BUILD=true ;;
|
|
--sync-qdrant) SYNC_QDRANT=true ;;
|
|
-h|--help)
|
|
sed -n '2,12p' "$0"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "알 수 없는 옵션: $arg (--help)"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:${PATH}"
|
|
|
|
log() { echo "[$(date '+%H:%M:%S')] $*"; }
|
|
fail() { log "ERROR: $*"; exit 1; }
|
|
|
|
wait_http() {
|
|
local url="$1" label="$2" max="${3:-90}"
|
|
local i=0
|
|
while (( i < max )); do
|
|
if curl -sf --max-time 3 "$url" >/dev/null 2>&1; then
|
|
log "✅ ${label}"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
i=$((i + 2))
|
|
done
|
|
log "⚠️ ${label} 타임아웃 (${url})"
|
|
return 1
|
|
}
|
|
|
|
wait_port() {
|
|
local port="$1" label="$2" max="${3:-90}"
|
|
local i=0
|
|
while (( i < max )); do
|
|
if lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
log "✅ ${label} (:${port})"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
i=$((i + 2))
|
|
done
|
|
log "⚠️ ${label} 타임아웃 (:${port})"
|
|
return 1
|
|
}
|
|
|
|
ai_tunnel_ready() {
|
|
curl -sf --max-time 3 "http://127.0.0.1:${LLM_PORT}/v1/models" >/dev/null 2>&1 \
|
|
&& curl -sf --max-time 3 "http://127.0.0.1:${EMBED_PORT}/health" >/dev/null 2>&1 \
|
|
&& curl -sf --max-time 3 -X POST "http://127.0.0.1:${RERANK_PORT}/rerank" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query":"ping","documents":["doc"]}' >/dev/null 2>&1
|
|
}
|
|
|
|
start_tunnel() {
|
|
if ai_tunnel_ready; then
|
|
log "exdev AI 터널 이미 연결됨 (:${LLM_PORT}~:${RERANK_PORT})"
|
|
return 0
|
|
fi
|
|
|
|
log "exdev AI SSH 터널 백그라운드 기동..."
|
|
ssh -f -N \
|
|
-o ServerAliveInterval=30 \
|
|
-o ExitOnForwardFailure=yes \
|
|
-L "${LLM_PORT}:127.0.0.1:${LLM_PORT}" \
|
|
-L "${EMBED_PORT}:127.0.0.1:${EMBED_PORT}" \
|
|
-L "${RERANK_PORT}:127.0.0.1:${RERANK_PORT}" \
|
|
"${EXDEV_USER:-aidev}@${EXDEV_HOST:-chatbot.exdev.co.kr}"
|
|
|
|
sleep 1
|
|
pgrep -f "ssh -f -N.*${LLM_PORT}:127.0.0.1:${LLM_PORT}" | head -1 > "${RUN_DIR}/tunnel.pid" || true
|
|
|
|
local i=0
|
|
while (( i < 60 )); do
|
|
if ai_tunnel_ready; then
|
|
log "✅ exdev AI 터널 (LLM/Embed/Rerank)"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
i=$((i + 2))
|
|
done
|
|
fail "exdev AI 터널 실패 — exdev에서 start-host-ai.sh 실행 여부 확인"
|
|
}
|
|
|
|
patch_env() {
|
|
local key="$1" value="$2"
|
|
local tmp="${RAG_ENV}.tmp.$$"
|
|
if [[ -f "${RAG_ENV}" ]]; then
|
|
grep -v "^${key}=" "${RAG_ENV}" > "${tmp}" || true
|
|
else
|
|
: > "${tmp}"
|
|
fi
|
|
printf '%s\n%s=%s\n' "$(cat "${tmp}")" "${key}" "${value}" > "${RAG_ENV}"
|
|
rm -f "${tmp}"
|
|
}
|
|
|
|
ensure_rag_env() {
|
|
if [[ ! -f "${RAG_ENV}" ]]; then
|
|
log ".env 없음 → env.exdev-ai.template 복사"
|
|
cp "${ENV_TEMPLATE}" "${RAG_ENV}"
|
|
fi
|
|
|
|
# Docker 컨테이너 → Mac 호스트 접근
|
|
patch_env "LLM_BASE_URL" "http://host.docker.internal:${LLM_PORT}"
|
|
patch_env "TEI_EMBED_URL" "http://host.docker.internal:${EMBED_PORT}"
|
|
patch_env "TEI_RERANK_URL" "http://host.docker.internal:${RERANK_PORT}"
|
|
patch_env "EMBED_API_STYLE" "tei"
|
|
patch_env "RERANK_API_STYLE" "legacy"
|
|
patch_env "MONGO_HOST" "host.docker.internal"
|
|
patch_env "CHATBOT_API_BASE_URL" "http://host.docker.internal:${WAS_PORT}/api"
|
|
patch_env "INTERNAL_TOOL_API_KEY" "${INTERNAL_TOOL_API_KEY}"
|
|
|
|
# 로컬 Mongo 무인증
|
|
patch_env "MONGO_USER" ""
|
|
patch_env "MONGO_PASSWORD" ""
|
|
}
|
|
|
|
ensure_mongo() {
|
|
if docker ps --format '{{.Names}}' | grep -qx "${MONGO_CONTAINER}"; then
|
|
docker start "${MONGO_CONTAINER}" >/dev/null 2>&1 || true
|
|
log "MongoDB 재사용: ${MONGO_CONTAINER}"
|
|
wait_port 27017 "MongoDB" 20 || true
|
|
return 0
|
|
fi
|
|
|
|
if lsof -iTCP:27017 -sTCP:LISTEN >/dev/null 2>&1; then
|
|
log "MongoDB :27017 이미 사용 중 (외부 인스턴스)"
|
|
return 0
|
|
fi
|
|
|
|
log "MongoDB 컨테이너 생성: ${MONGO_CONTAINER}"
|
|
docker run -d --name "${MONGO_CONTAINER}" \
|
|
-p 27017:27017 \
|
|
mongo:7 >/dev/null
|
|
wait_port 27017 "MongoDB" 30 || true
|
|
}
|
|
|
|
start_rag() {
|
|
ensure_rag_env
|
|
[[ -f "${COMPOSE_LOCAL}" ]] || fail "docker-compose.local.yml 없음"
|
|
|
|
cd "${RAG_DIR}"
|
|
|
|
local -a up_flags=(-d)
|
|
if $NO_BUILD; then
|
|
up_flags+=(--no-build)
|
|
else
|
|
up_flags+=(--build)
|
|
fi
|
|
|
|
# 이전 slim/legacy 잔여 정리
|
|
docker compose -f docker-compose-slim.yml down --remove-orphans 2>/dev/null || true
|
|
|
|
log "RAG 스택 기동 (docker-compose.local.yml)..."
|
|
docker compose -f docker-compose.local.yml up "${up_flags[@]}"
|
|
|
|
wait_http "http://127.0.0.1:6335/readyz" "Qdrant" 30 || \
|
|
wait_http "http://127.0.0.1:6335/collections" "Qdrant" 30 || true
|
|
wait_http "http://127.0.0.1:${RAG_PORT}/health" "RAG API" 120
|
|
wait_http "http://127.0.0.1:${ADMIN_PORT}/api/stats" "Vector Admin" 60 || \
|
|
wait_port "${ADMIN_PORT}" "Vector Admin" 30 || true
|
|
|
|
local vc
|
|
vc="$(curl -sf "http://127.0.0.1:${RAG_PORT}/health" 2>/dev/null \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('vector_count',0))" 2>/dev/null || echo 0)"
|
|
if $SYNC_QDRANT || [[ "${vc}" == "0" ]]; then
|
|
log "Qdrant 데이터 exdev 동기화 (vector_count=${vc})..."
|
|
chmod +x "${LOCAL_DEV_DIR}/sync-qdrant-from-exdev.sh"
|
|
"${LOCAL_DEV_DIR}/sync-qdrant-from-exdev.sh" || log "⚠️ Qdrant 동기화 실패 — 수동: ./local-dev/sync-qdrant-from-exdev.sh"
|
|
fi
|
|
}
|
|
|
|
ensure_java() {
|
|
local home=""
|
|
local candidates=(
|
|
"/Users/macbook/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
|
|
"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
|
|
"/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
|
|
)
|
|
for candidate in "${candidates[@]}"; do
|
|
if [[ -x "${candidate}/bin/java" ]]; then
|
|
home="${candidate}"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "${home}" ]] && /usr/libexec/java_home -v 17 >/dev/null 2>&1; then
|
|
home="$(/usr/libexec/java_home -v 17)"
|
|
fi
|
|
if [[ -z "${home}" ]]; then
|
|
fail "Java 17 필요 — brew install openjdk@17"
|
|
fi
|
|
export JAVA_HOME="${home}"
|
|
export PATH="${JAVA_HOME}/bin:${PATH}"
|
|
log "Java 17: $("${JAVA_HOME}/bin/java" -version 2>&1 | head -1)"
|
|
}
|
|
|
|
start_java_service() {
|
|
local name="$1" dir="$2" pid_file="$3" log_file="$4"
|
|
shift 4
|
|
|
|
if [[ -f "${pid_file}" ]]; then
|
|
local old_pid
|
|
old_pid="$(cat "${pid_file}")"
|
|
if kill -0 "$old_pid" 2>/dev/null; then
|
|
log "${name} 이미 실행 중 (PID ${old_pid})"
|
|
return 0
|
|
fi
|
|
rm -f "${pid_file}"
|
|
fi
|
|
|
|
chmod +x "${dir}/gradlew"
|
|
|
|
log "${name} 기동 중..."
|
|
: > "${log_file}"
|
|
(
|
|
cd "${dir}"
|
|
export JAVA_HOME
|
|
for ev in "$@"; do
|
|
export "${ev?}"
|
|
done
|
|
nohup ./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" bootRun \
|
|
>> "${log_file}" 2>&1 &
|
|
echo $! > "${pid_file}"
|
|
)
|
|
log "${name} PID $(cat "${pid_file}") → ${log_file}"
|
|
}
|
|
|
|
verify_stack() {
|
|
local ok=true
|
|
|
|
ai_tunnel_ready || { log "❌ exdev AI 터널"; ok=false; }
|
|
curl -sf "http://127.0.0.1:${RAG_PORT}/health" | grep -q '"status"' || { log "❌ RAG health"; ok=false; }
|
|
wait_port "${WAS_PORT}" "chatbotApi" 5 || ok=false
|
|
wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 5 || ok=false
|
|
curl -sf "http://127.0.0.1:${APP_PORT}/chatbot/app/" | grep -q 'data-chatbot-app' || { log "❌ chatbotApp UI"; ok=false; }
|
|
|
|
if $ok; then
|
|
log "✅ 전체 스택 정상"
|
|
return 0
|
|
fi
|
|
log "⚠️ 일부 서비스 미응답 — ${LOG_DIR}/ 로그 확인"
|
|
return 1
|
|
}
|
|
|
|
# ── main ──
|
|
mkdir -p "${RUN_DIR}" "${LOG_DIR}"
|
|
|
|
docker info >/dev/null 2>&1 || fail "Docker Engine 미기동"
|
|
|
|
log "=========================================="
|
|
log "exChatbot 로컬 전체 스택 기동"
|
|
log "=========================================="
|
|
|
|
if ! $SKIP_TUNNEL; then
|
|
start_tunnel
|
|
else
|
|
ai_tunnel_ready || fail "--no-tunnel 이지만 :${LLM_PORT}~:${RERANK_PORT} AI 미응답"
|
|
fi
|
|
|
|
ensure_mongo
|
|
start_rag
|
|
|
|
if ! $SKIP_JAVA; then
|
|
ensure_java
|
|
start_java_service "chatbotApi" "${API_DIR}" \
|
|
"${RUN_DIR}/chatbot-api.pid" "${LOG_DIR}/chatbot-api.log" \
|
|
"INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \
|
|
"SKILL_INTERNAL_BASE_URL=http://127.0.0.1:${SKILL_PORT}" \
|
|
"RAG_SERVER_HOST=127.0.0.1" \
|
|
"RAG_SERVER_PORT=${RAG_PORT}"
|
|
|
|
start_java_service "kakaoChatbotSkill" "${SKILL_DIR}" \
|
|
"${RUN_DIR}/kakao-skill.pid" "${LOG_DIR}/kakao-skill.log" \
|
|
"AGENT_MODE=agent" \
|
|
"RAG_AGENT_CHAT_URL=http://127.0.0.1:${RAG_PORT}/agent/chat" \
|
|
"CHATBOT_API_BASE_URL=http://127.0.0.1:${WAS_PORT}/api/" \
|
|
"INTERNAL_TOOL_API_KEY=${INTERNAL_TOOL_API_KEY}" \
|
|
"CHATBOT_APP_URL=http://localhost:${APP_PORT}/chatbot/app/"
|
|
|
|
wait_port "${WAS_PORT}" "chatbotApi" 240 || true
|
|
wait_port "${SKILL_PORT}" "kakaoChatbotSkill" 240 || true
|
|
fi
|
|
|
|
log "chatbotApp 기동..."
|
|
docker rm -f ex-chatbot-app-web 2>/dev/null || true
|
|
(
|
|
cd "${APP_DIR}"
|
|
export CHATBOT_API_UPSTREAM="http://host.docker.internal:${SKILL_PORT}/web/"
|
|
export CHATBOT_API_HOST="host.docker.internal"
|
|
docker compose up -d --force-recreate
|
|
)
|
|
|
|
wait_http "http://127.0.0.1:${APP_PORT}/chatbot/app/" "chatbotApp UI" 30 || true
|
|
|
|
log "=========================================="
|
|
log "기동 완료"
|
|
log " UI http://127.0.0.1:${APP_PORT}/chatbot/app/"
|
|
log " RAG http://127.0.0.1:${RAG_PORT}/health"
|
|
log " Admin http://127.0.0.1:${ADMIN_PORT}/"
|
|
log " Qdrant http://127.0.0.1:6335/ (호스트 포트)"
|
|
log " WAS http://127.0.0.1:${WAS_PORT}/"
|
|
log " Skill http://127.0.0.1:${SKILL_PORT}/web/ask"
|
|
log " exdev AI tunnel :${LLM_PORT} ~ :${RERANK_PORT}"
|
|
log " 로그 ${LOG_DIR}/"
|
|
log " 종료 ./local-dev/stop-all.sh"
|
|
log "=========================================="
|
|
|
|
HEALTH="$(curl -sf "http://127.0.0.1:${RAG_PORT}/health" 2>/dev/null || echo 'unavailable')"
|
|
log "RAG health: ${HEALTH}"
|
|
|
|
verify_stack || exit 1
|