CASTAD-v0.1/startserver.sh

1169 lines
36 KiB
Bash

#!/bin/bash
# ============================================
# CaStAD Production Server - 완전 자동 배포 스크립트
# ============================================
# 도메인:
# - castad.ktenterprise.net
# - ado2.whitedonkey.kr
# 서버: qm391-0282.cafe24.com (Ubuntu 24.04)
# ============================================
set -e # 오류 시 중단
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
log() { echo -e "${GREEN}[✓]${NC} $1"; }
error() { echo -e "${RED}[✗]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
info() { echo -e "${CYAN}[i]${NC} $1"; }
header() { echo -e "\n${BOLD}${BLUE}═══ $1 ═══${NC}\n"; }
# ============================================
# 설정
# ============================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# 포트 설정 (기본값 - 충돌 시 자동으로 다른 포트 탐색)
BACKEND_PORT_DEFAULT=3001
INSTAGRAM_PORT_DEFAULT=5003
FRONTEND_PORT=3002 # 빌드 시에만 사용
# 실제 사용할 포트 (find_available_ports에서 설정)
BACKEND_PORT=$BACKEND_PORT_DEFAULT
INSTAGRAM_PORT=$INSTAGRAM_PORT_DEFAULT
# 도메인
DOMAIN_PRIMARY="castad.ktenterprise.net"
DOMAIN_SECONDARY="ado2.whitedonkey.kr"
# 디렉토리
APP_DIR="$SCRIPT_DIR"
NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"
LOG_DIR="$APP_DIR/logs"
# 명령어
ACTION=${1:-help}
# ============================================
# 배너 출력
# ============================================
show_banner() {
echo ""
echo -e "${BOLD}${CYAN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}${CYAN}║ 🍮 CaStAD (카스타드) Production Server ║${NC}"
echo -e "${BOLD}${CYAN}║ AI-Powered Pension Marketing Video Platform ║${NC}"
echo -e "${BOLD}${CYAN}╠════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BOLD}${CYAN}║ 도메인: ${DOMAIN_PRIMARY}${NC}"
echo -e "${BOLD}${CYAN}${DOMAIN_SECONDARY}${NC}"
echo -e "${BOLD}${CYAN}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
# ============================================
# 환경 검사
# ============================================
check_environment() {
header "환경 검사"
local ERRORS=0
# Node.js
if command -v node &> /dev/null; then
local NODE_VER=$(node -v)
log "Node.js: $NODE_VER"
else
error "Node.js가 설치되지 않았습니다"
ERRORS=$((ERRORS + 1))
fi
# npm
if command -v npm &> /dev/null; then
local NPM_VER=$(npm -v)
log "npm: v$NPM_VER"
else
error "npm이 설치되지 않았습니다"
ERRORS=$((ERRORS + 1))
fi
# PM2
if command -v pm2 &> /dev/null; then
local PM2_VER=$(pm2 -v)
log "PM2: v$PM2_VER"
else
warn "PM2가 설치되지 않았습니다. 설치합니다..."
npm install -g pm2
log "PM2 설치 완료"
fi
# Python3
if command -v python3 &> /dev/null; then
local PY_VER=$(python3 --version)
log "Python: $PY_VER"
else
error "Python3이 설치되지 않았습니다"
ERRORS=$((ERRORS + 1))
fi
# pip3
if command -v pip3 &> /dev/null; then
log "pip3: 설치됨"
else
warn "pip3이 설치되지 않았습니다"
fi
# FFmpeg
if command -v ffmpeg &> /dev/null; then
log "FFmpeg: 설치됨"
else
warn "FFmpeg가 설치되지 않았습니다 - 영상 렌더링 불가"
info "설치: sudo apt-get install -y ffmpeg"
fi
# Chromium (Puppeteer용)
if command -v chromium-browser &> /dev/null || command -v chromium &> /dev/null; then
log "Chromium: 설치됨"
else
warn "Chromium이 설치되지 않았습니다 - 영상 렌더링 불가"
info "설치: sudo apt-get install -y chromium-browser"
fi
# nginx
if command -v nginx &> /dev/null; then
local NGINX_VER=$(nginx -v 2>&1 | cut -d'/' -f2)
log "nginx: $NGINX_VER"
else
error "nginx가 설치되지 않았습니다"
ERRORS=$((ERRORS + 1))
fi
# Git
if command -v git &> /dev/null; then
log "Git: 설치됨"
else
error "Git이 설치되지 않았습니다"
ERRORS=$((ERRORS + 1))
fi
if [ $ERRORS -gt 0 ]; then
error "필수 요구사항 $ERRORS 개가 누락되었습니다"
return 1
fi
log "환경 검사 완료"
return 0
}
# ============================================
# 포트 사용 가능 여부 확인
# ============================================
is_port_available() {
local PORT=$1
# ss 명령어로 포트 사용 여부 확인
if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then
return 1 # 사용 중
fi
return 0 # 사용 가능
}
# ============================================
# 사용 가능한 포트 찾기
# ============================================
find_free_port() {
local START_PORT=$1
local MAX_TRIES=${2:-20}
local PORT=$START_PORT
for ((i=0; i<MAX_TRIES; i++)); do
if is_port_available $PORT; then
echo $PORT
return 0
fi
PORT=$((PORT + 1))
done
# 찾지 못함
echo ""
return 1
}
# ============================================
# 포트 자동 탐색 및 설정
# ============================================
find_available_ports() {
header "포트 자동 탐색"
# CaStAD PM2 프로세스가 사용 중인 포트는 재사용 가능 (재시작할 것이므로)
local CASTAD_BACKEND_PID=$(pm2 pid castad-backend 2>/dev/null || echo "")
local CASTAD_INSTA_PID=$(pm2 pid castad-instagram 2>/dev/null || echo "")
# Backend 포트 찾기
if is_port_available $BACKEND_PORT_DEFAULT; then
BACKEND_PORT=$BACKEND_PORT_DEFAULT
log "Backend 포트: ${BACKEND_PORT} (기본값 사용)"
else
# 기존 CaStAD 프로세스인지 확인
local PORT_PID=$(ss -tlnp 2>/dev/null | grep ":${BACKEND_PORT_DEFAULT} " | grep -oP 'pid=\K[0-9]+' | head -1)
if [ -n "$CASTAD_BACKEND_PID" ] && [ "$PORT_PID" == "$CASTAD_BACKEND_PID" ]; then
BACKEND_PORT=$BACKEND_PORT_DEFAULT
info "Backend 포트: ${BACKEND_PORT} (기존 CaStAD 프로세스, 재시작 시 교체)"
else
# 다른 프로세스가 사용 중 - 새 포트 찾기
local NEW_PORT=$(find_free_port $BACKEND_PORT_DEFAULT)
if [ -z "$NEW_PORT" ]; then
error "사용 가능한 Backend 포트를 찾을 수 없습니다 (${BACKEND_PORT_DEFAULT}~$((BACKEND_PORT_DEFAULT+19)))"
return 1
fi
BACKEND_PORT=$NEW_PORT
warn "Backend 포트 ${BACKEND_PORT_DEFAULT} 사용 중 → ${BACKEND_PORT} 로 변경"
fi
fi
# Instagram 서비스 포트 찾기
if is_port_available $INSTAGRAM_PORT_DEFAULT; then
INSTAGRAM_PORT=$INSTAGRAM_PORT_DEFAULT
log "Instagram 포트: ${INSTAGRAM_PORT} (기본값 사용)"
else
# 기존 CaStAD 프로세스인지 확인
local PORT_PID=$(ss -tlnp 2>/dev/null | grep ":${INSTAGRAM_PORT_DEFAULT} " | grep -oP 'pid=\K[0-9]+' | head -1)
if [ -n "$CASTAD_INSTA_PID" ] && [ "$PORT_PID" == "$CASTAD_INSTA_PID" ]; then
INSTAGRAM_PORT=$INSTAGRAM_PORT_DEFAULT
info "Instagram 포트: ${INSTAGRAM_PORT} (기존 CaStAD 프로세스, 재시작 시 교체)"
else
# 다른 프로세스가 사용 중 - 새 포트 찾기
local NEW_PORT=$(find_free_port $INSTAGRAM_PORT_DEFAULT)
if [ -z "$NEW_PORT" ]; then
error "사용 가능한 Instagram 포트를 찾을 수 없습니다 (${INSTAGRAM_PORT_DEFAULT}~$((INSTAGRAM_PORT_DEFAULT+19)))"
return 1
fi
INSTAGRAM_PORT=$NEW_PORT
warn "Instagram 포트 ${INSTAGRAM_PORT_DEFAULT} 사용 중 → ${INSTAGRAM_PORT} 로 변경"
fi
fi
echo ""
info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
info " 최종 포트 설정:"
info " Backend: ${BACKEND_PORT}"
info " Instagram: ${INSTAGRAM_PORT}"
info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "포트 탐색 완료"
return 0
}
# ============================================
# 의존성 설치
# ============================================
install_dependencies() {
header "의존성 설치"
# Node.js 의존성
if [ ! -d "node_modules" ] || [ "$1" == "force" ]; then
log "Frontend 패키지 설치 중..."
npm install --legacy-peer-deps --silent
else
info "Frontend 패키지 이미 설치됨 (강제 설치: --force)"
fi
if [ ! -d "server/node_modules" ] || [ "$1" == "force" ]; then
log "Backend 패키지 설치 중..."
cd server && npm install --legacy-peer-deps --silent && cd ..
else
info "Backend 패키지 이미 설치됨"
fi
# Python 의존성 (Instagram 서비스)
if [ -f "server/instagram/requirements.txt" ]; then
if ! python3 -c "import instagrapi" 2>/dev/null; then
log "Instagram 서비스 Python 패키지 설치 중..."
pip3 install -q -r server/instagram/requirements.txt 2>/dev/null || \
pip3 install --break-system-packages -q -r server/instagram/requirements.txt 2>/dev/null || true
else
info "Instagram Python 패키지 이미 설치됨"
fi
fi
log "의존성 설치 완료"
}
# ============================================
# 디렉토리 설정
# ============================================
setup_directories() {
header "디렉토리 설정"
mkdir -p "$LOG_DIR"
mkdir -p server/downloads
mkdir -p server/temp
mkdir -p server/uploads
log "디렉토리 생성 완료"
}
# ============================================
# 환경 변수 검사
# ============================================
check_env_file() {
header "환경 변수 검사"
if [ ! -f ".env" ]; then
if [ -f ".env.production.example" ]; then
warn ".env 파일이 없습니다. 템플릿에서 생성합니다..."
cp .env.production.example .env
warn ".env 파일을 편집하여 API 키를 설정하세요!"
warn "nano .env"
else
error ".env 파일이 없습니다!"
return 1
fi
else
log ".env 파일 확인됨"
fi
# 필수 환경변수 검사
source .env 2>/dev/null || true
if [ -z "$JWT_SECRET" ] || [ "$JWT_SECRET" == "your-super-secret-jwt-key-change-this-in-production" ]; then
warn "JWT_SECRET이 기본값입니다. 변경하세요!"
fi
if [ -z "$VITE_GEMINI_API_KEY" ] || [ "$VITE_GEMINI_API_KEY" == "your-gemini-api-key" ]; then
warn "VITE_GEMINI_API_KEY가 설정되지 않았습니다"
fi
# 포트 환경변수 업데이트
local ENV_UPDATED=0
if grep -q "^PORT=" .env; then
local OLD_PORT=$(grep "^PORT=" .env | cut -d'=' -f2)
if [ "$OLD_PORT" != "${BACKEND_PORT}" ]; then
sed -i "s/^PORT=.*/PORT=${BACKEND_PORT}/" .env
info ".env PORT: ${OLD_PORT}${BACKEND_PORT}"
ENV_UPDATED=1
fi
else
echo "PORT=${BACKEND_PORT}" >> .env
info ".env PORT: ${BACKEND_PORT} (추가)"
ENV_UPDATED=1
fi
if grep -q "^INSTAGRAM_SERVICE_PORT=" .env; then
local OLD_PORT=$(grep "^INSTAGRAM_SERVICE_PORT=" .env | cut -d'=' -f2)
if [ "$OLD_PORT" != "${INSTAGRAM_PORT}" ]; then
sed -i "s/^INSTAGRAM_SERVICE_PORT=.*/INSTAGRAM_SERVICE_PORT=${INSTAGRAM_PORT}/" .env
info ".env INSTAGRAM_SERVICE_PORT: ${OLD_PORT}${INSTAGRAM_PORT}"
ENV_UPDATED=1
fi
else
echo "INSTAGRAM_SERVICE_PORT=${INSTAGRAM_PORT}" >> .env
info ".env INSTAGRAM_SERVICE_PORT: ${INSTAGRAM_PORT} (추가)"
ENV_UPDATED=1
fi
if grep -q "^INSTAGRAM_SERVICE_URL=" .env; then
sed -i "s|^INSTAGRAM_SERVICE_URL=.*|INSTAGRAM_SERVICE_URL=http://localhost:${INSTAGRAM_PORT}|" .env
else
echo "INSTAGRAM_SERVICE_URL=http://localhost:${INSTAGRAM_PORT}" >> .env
fi
if [ $ENV_UPDATED -eq 1 ]; then
log ".env 파일 포트 설정 업데이트 완료"
else
log ".env 파일 포트 설정 확인 완료 (변경 없음)"
fi
}
# ============================================
# 프론트엔드 빌드
# ============================================
build_frontend() {
header "프론트엔드 빌드"
export NODE_ENV=production
export VITE_BACKEND_PORT=$BACKEND_PORT
log "Vite 빌드 중..."
npm run build
if [ ! -d "dist" ]; then
error "프론트엔드 빌드 실패!"
return 1
fi
log "프론트엔드 빌드 완료 (dist/)"
}
# ============================================
# PM2 Ecosystem 파일 생성
# ============================================
create_ecosystem() {
header "PM2 설정 생성"
cat > ecosystem.config.cjs << ECOSYSTEM
module.exports = {
apps: [
{
name: 'castad-backend',
script: 'server/index.js',
cwd: '${APP_DIR}',
instances: 1,
exec_mode: 'fork',
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: ${BACKEND_PORT}
},
error_file: '${LOG_DIR}/backend-error.log',
out_file: '${LOG_DIR}/backend-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
},
{
name: 'castad-instagram',
script: 'server/instagram/instagram_service.py',
cwd: '${APP_DIR}',
interpreter: 'python3',
instances: 1,
autorestart: true,
watch: false,
env: {
INSTAGRAM_SERVICE_PORT: ${INSTAGRAM_PORT}
},
error_file: '${LOG_DIR}/instagram-error.log',
out_file: '${LOG_DIR}/instagram-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}
]
};
ECOSYSTEM
log "ecosystem.config.cjs 생성됨"
}
# ============================================
# nginx 설정 생성
# ============================================
create_nginx_config() {
header "nginx 설정 생성"
local NGINX_CONF="nginx/castad-server.conf"
cat > "$NGINX_CONF" << 'NGINX_CONFIG'
# ============================================
# CaStAD Nginx Configuration
# Generated by startserver.sh
# ============================================
upstream castad_backend {
server 127.0.0.1:BACKEND_PORT_PLACEHOLDER;
keepalive 64;
}
# HTTP → HTTPS 리다이렉트
server {
listen 80;
listen [::]:80;
server_name DOMAIN_PRIMARY_PLACEHOLDER DOMAIN_SECONDARY_PLACEHOLDER;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 서버
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name DOMAIN_PRIMARY_PLACEHOLDER DOMAIN_SECONDARY_PLACEHOLDER;
# SSL 인증서 (certbot이 자동 설정)
ssl_certificate /etc/letsencrypt/live/DOMAIN_PRIMARY_PLACEHOLDER/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PRIMARY_PLACEHOLDER/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 보안 헤더
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 로그
access_log /var/log/nginx/castad_access.log;
error_log /var/log/nginx/castad_error.log;
# 파일 업로드 크기
client_max_body_size 500M;
client_body_timeout 300s;
# 정적 파일 (프론트엔드)
root APP_DIR_PLACEHOLDER/dist;
index index.html;
# Gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml;
# 정적 자원 캐싱
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# API 요청
location /api/ {
proxy_pass http://castad_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
}
# 렌더링 요청 (긴 타임아웃)
location /render {
proxy_pass http://castad_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
proxy_buffering off;
}
# 다운로드/업로드/임시 파일
location ~ ^/(downloads|temp|uploads)/ {
proxy_pass http://castad_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# SPA 라우팅
location / {
try_files $uri $uri/ /index.html;
}
}
NGINX_CONFIG
# 플레이스홀더 치환
sed -i "s|BACKEND_PORT_PLACEHOLDER|${BACKEND_PORT}|g" "$NGINX_CONF"
sed -i "s|DOMAIN_PRIMARY_PLACEHOLDER|${DOMAIN_PRIMARY}|g" "$NGINX_CONF"
sed -i "s|DOMAIN_SECONDARY_PLACEHOLDER|${DOMAIN_SECONDARY}|g" "$NGINX_CONF"
sed -i "s|APP_DIR_PLACEHOLDER|${APP_DIR}|g" "$NGINX_CONF"
log "nginx 설정 파일 생성: $NGINX_CONF"
}
# ============================================
# nginx 설정 설치
# ============================================
install_nginx_config() {
header "nginx 설정 설치"
local NGINX_CONF="nginx/castad-server.conf"
local NGINX_NAME="castad"
if [ ! -f "$NGINX_CONF" ]; then
create_nginx_config
fi
# root 권한 확인
if [ "$EUID" -ne 0 ]; then
warn "nginx 설정 설치에 sudo 권한이 필요합니다"
info "수동 설치:"
info " sudo cp $NGINX_CONF $NGINX_AVAILABLE/$NGINX_NAME"
info " sudo ln -sf $NGINX_AVAILABLE/$NGINX_NAME $NGINX_ENABLED/"
info " sudo nginx -t && sudo systemctl reload nginx"
return 0
fi
# 설정 복사
cp "$NGINX_CONF" "$NGINX_AVAILABLE/$NGINX_NAME"
# 심볼릭 링크
ln -sf "$NGINX_AVAILABLE/$NGINX_NAME" "$NGINX_ENABLED/"
# 설정 검증
if nginx -t 2>/dev/null; then
systemctl reload nginx
log "nginx 설정 설치 및 적용 완료"
else
error "nginx 설정 오류!"
nginx -t
return 1
fi
}
# ============================================
# DNS 확인
# ============================================
check_dns() {
header "DNS 확인"
local SERVER_IP=$(curl -s ifconfig.me 2>/dev/null || curl -s icanhazip.com 2>/dev/null)
log "서버 IP: ${SERVER_IP}"
local DNS_OK=1
# Primary 도메인 확인
local PRIMARY_IP=$(dig +short "$DOMAIN_PRIMARY" 2>/dev/null | head -1)
if [ -z "$PRIMARY_IP" ]; then
error "${DOMAIN_PRIMARY}: DNS 레코드 없음"
DNS_OK=0
elif [ "$PRIMARY_IP" == "$SERVER_IP" ]; then
log "${DOMAIN_PRIMARY}${PRIMARY_IP}"
else
warn "${DOMAIN_PRIMARY}${PRIMARY_IP} (서버 IP와 다름: ${SERVER_IP})"
DNS_OK=0
fi
# Secondary 도메인 확인
local SECONDARY_IP=$(dig +short "$DOMAIN_SECONDARY" 2>/dev/null | head -1)
if [ -z "$SECONDARY_IP" ]; then
error "${DOMAIN_SECONDARY}: DNS 레코드 없음"
DNS_OK=0
elif [ "$SECONDARY_IP" == "$SERVER_IP" ]; then
log "${DOMAIN_SECONDARY}${SECONDARY_IP}"
else
warn "${DOMAIN_SECONDARY}${SECONDARY_IP} (서버 IP와 다름: ${SERVER_IP})"
DNS_OK=0
fi
if [ $DNS_OK -eq 0 ]; then
warn "DNS 설정을 확인하세요. SSL 발급이 실패할 수 있습니다."
return 1
fi
log "DNS 확인 완료"
return 0
}
# ============================================
# HTTP 전용 nginx 설정 생성 (SSL 발급 전용)
# ============================================
create_nginx_http_only() {
header "HTTP 전용 nginx 설정 생성"
local NGINX_CONF="nginx/castad-http.conf"
cat > "$NGINX_CONF" << NGINX_HTTP
# CaStAD HTTP-only config (for SSL certificate issuance)
server {
listen 80;
listen [::]:80;
server_name ${DOMAIN_PRIMARY} ${DOMAIN_SECONDARY};
# Let's Encrypt 인증용
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# API 프록시
location /api/ {
proxy_pass http://127.0.0.1:${BACKEND_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# 렌더링
location /render {
proxy_pass http://127.0.0.1:${BACKEND_PORT};
proxy_read_timeout 600s;
}
# 다운로드/업로드/임시
location ~ ^/(downloads|temp|uploads)/ {
proxy_pass http://127.0.0.1:${BACKEND_PORT};
}
# 정적 파일
root ${APP_DIR}/dist;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
}
NGINX_HTTP
log "HTTP 전용 nginx 설정 생성: $NGINX_CONF"
}
# ============================================
# SSL 인증서 설치 (전체 자동화)
# ============================================
install_ssl() {
header "SSL 인증서 설치"
# root 권한 확인
if [ "$EUID" -ne 0 ]; then
warn "SSL 인증서 설치에 sudo 권한이 필요합니다"
info "실행: sudo ./startserver.sh ssl"
return 0
fi
# DNS 확인
check_dns || {
warn "DNS 문제가 있지만 계속 진행합니다..."
}
# certbot 설치 확인 및 자동 설치
if ! command -v certbot &> /dev/null; then
log "certbot 설치 중..."
apt-get update -qq
apt-get install -y -qq certbot python3-certbot-nginx
log "certbot 설치 완료"
else
log "certbot: 이미 설치됨"
fi
# certbot 디렉토리 생성
mkdir -p /var/www/certbot
# 기존 SSL 인증서 확인
if [ -f "/etc/letsencrypt/live/${DOMAIN_PRIMARY}/fullchain.pem" ]; then
log "기존 SSL 인증서 발견"
info "갱신 시도 중..."
certbot renew --quiet
log "SSL 인증서 갱신 완료"
else
# HTTP 전용 설정으로 먼저 nginx 구성
log "HTTP 전용 nginx 설정 적용 중..."
create_nginx_http_only
# nginx에 HTTP 설정 적용
cp nginx/castad-http.conf /etc/nginx/sites-available/castad
ln -sf /etc/nginx/sites-available/castad /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
if nginx -t 2>/dev/null; then
systemctl reload nginx
log "HTTP nginx 설정 적용 완료"
else
error "nginx 설정 오류"
nginx -t
return 1
fi
# 잠시 대기 (nginx 재시작 완료 대기)
sleep 2
# SSL 인증서 발급
log "SSL 인증서 발급 중..."
if certbot --nginx \
-d "$DOMAIN_PRIMARY" \
-d "$DOMAIN_SECONDARY" \
--non-interactive \
--agree-tos \
--email "admin@${DOMAIN_PRIMARY}" \
--redirect; then
log "SSL 인증서 발급 성공!"
else
error "SSL 인증서 발급 실패"
info "수동으로 시도하세요: sudo certbot --nginx -d $DOMAIN_PRIMARY -d $DOMAIN_SECONDARY"
return 1
fi
fi
# HTTPS nginx 설정 적용
log "HTTPS nginx 설정 적용 중..."
create_nginx_config
cp nginx/castad-server.conf /etc/nginx/sites-available/castad
if nginx -t 2>/dev/null; then
systemctl reload nginx
log "HTTPS nginx 설정 적용 완료"
else
warn "HTTPS 설정 오류 - certbot이 자동 생성한 설정 사용"
fi
# 자동 갱신 확인
log "자동 갱신 테스트..."
certbot renew --dry-run --quiet && log "자동 갱신 테스트 성공" || warn "자동 갱신 테스트 실패"
echo ""
log "🔒 SSL 설치 완료!"
info " https://${DOMAIN_PRIMARY}"
info " https://${DOMAIN_SECONDARY}"
}
# ============================================
# 포트를 사용 중인 프로세스 강제 종료
# ============================================
kill_port_process() {
local PORT=$1
local PID=$(lsof -t -i:${PORT} 2>/dev/null || ss -tlnp 2>/dev/null | grep ":${PORT} " | grep -oP 'pid=\K[0-9]+' | head -1)
if [ -n "$PID" ]; then
info "포트 ${PORT} 사용 중인 프로세스(PID: ${PID}) 종료 중..."
kill -9 $PID 2>/dev/null || true
sleep 1
return 0
fi
return 1
}
# ============================================
# PM2로 서비스 시작
# ============================================
start_services() {
header "서비스 시작"
# 기존 PM2 프로세스 정리
log "기존 PM2 프로세스 정리 중..."
pm2 delete castad-backend 2>/dev/null || true
pm2 delete castad-instagram 2>/dev/null || true
# PM2 데몬 완전 정리 (좀비 프로세스 제거)
pm2 kill 2>/dev/null || true
sleep 2
# 포트를 점유하고 있는 모든 프로세스 강제 종료
log "포트 점유 프로세스 정리 중..."
kill_port_process ${BACKEND_PORT} || true
kill_port_process ${INSTAGRAM_PORT} || true
# 추가로 3003 포트도 정리 (과거 설정으로 인한 좀비 프로세스)
kill_port_process 3003 || true
kill_port_process 3001 || true
kill_port_process 5003 || true
kill_port_process 5001 || true
sleep 1
# PM2로 시작
log "PM2 서비스 시작 중..."
pm2 start ecosystem.config.cjs
# 상태 저장
pm2 save --force
# 잠시 대기 (서비스 안정화)
sleep 5
# 헬스 체크
local RETRY=0
local MAX_RETRY=3
while [ $RETRY -lt $MAX_RETRY ]; do
if curl -s "http://localhost:${BACKEND_PORT}/api/admin/stats" > /dev/null 2>&1; then
log "Backend 서비스 정상 동작 (포트: ${BACKEND_PORT})"
break
fi
RETRY=$((RETRY + 1))
if [ $RETRY -lt $MAX_RETRY ]; then
info "Backend 응답 대기 중... (${RETRY}/${MAX_RETRY})"
sleep 3
fi
done
if [ $RETRY -eq $MAX_RETRY ]; then
warn "Backend 서비스 응답 없음 - 로그를 확인하세요"
warn "pm2 logs castad-backend --lines 20"
fi
# nginx 포트 자동 동기화
sync_nginx_port
log "서비스 시작 완료"
}
# ============================================
# nginx 포트 자동 동기화
# ============================================
sync_nginx_port() {
if [ ! -f "/etc/nginx/sites-enabled/castad" ]; then
return 0
fi
local CURRENT_NGINX_PORT=$(grep -oP 'server 127\.0\.0\.1:\K[0-9]+' /etc/nginx/sites-enabled/castad 2>/dev/null | head -1)
if [ -z "$CURRENT_NGINX_PORT" ]; then
return 0
fi
if [ "$CURRENT_NGINX_PORT" == "$BACKEND_PORT" ]; then
log "nginx 포트: ${BACKEND_PORT} (정상)"
return 0
fi
warn "nginx 포트 불일치 감지: ${CURRENT_NGINX_PORT}${BACKEND_PORT}"
if [ "$EUID" -eq 0 ]; then
sed -i "s/127.0.0.1:${CURRENT_NGINX_PORT}/127.0.0.1:${BACKEND_PORT}/g" /etc/nginx/sites-enabled/castad
if nginx -t 2>/dev/null; then
systemctl reload nginx
log "nginx 포트 자동 수정 완료: ${BACKEND_PORT}"
else
error "nginx 설정 오류"
fi
else
info "nginx 포트 수정이 필요합니다 (sudo 권한 필요):"
echo ""
echo " sudo sed -i 's/127.0.0.1:${CURRENT_NGINX_PORT}/127.0.0.1:${BACKEND_PORT}/g' /etc/nginx/sites-enabled/castad"
echo " sudo nginx -t && sudo systemctl reload nginx"
echo ""
fi
}
# ============================================
# 서비스 중지
# ============================================
stop_services() {
header "서비스 중지"
pm2 delete castad-backend 2>/dev/null || true
pm2 delete castad-instagram 2>/dev/null || true
pm2 save --force
log "서비스 중지 완료"
}
# ============================================
# 상태 표시
# ============================================
show_status() {
header "서비스 상태"
pm2 status
echo ""
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}Backend${NC}: http://localhost:${BACKEND_PORT}"
echo -e " ${BOLD}Instagram${NC}: http://localhost:${INSTAGRAM_PORT}"
echo -e " ${BOLD}도메인${NC}:"
echo -e " - https://${DOMAIN_PRIMARY}"
echo -e " - https://${DOMAIN_SECONDARY}"
echo -e " ${BOLD}관리자${NC}: admin / admin123"
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
echo ""
}
# ============================================
# 전체 설치 및 시작
# ============================================
full_install() {
show_banner
check_environment || exit 1
find_available_ports || exit 1
setup_directories
check_env_file || exit 1
install_dependencies
build_frontend || exit 1
create_ecosystem
# nginx 설정 (SSL 인증서 존재 여부에 따라 다름)
if [ "$EUID" -eq 0 ]; then
# SSL 인증서가 이미 있으면 HTTPS 설정, 없으면 HTTP만
if [ -f "/etc/letsencrypt/live/${DOMAIN_PRIMARY}/fullchain.pem" ]; then
log "기존 SSL 인증서 발견 - HTTPS 설정 적용"
create_nginx_config
install_nginx_config
else
log "SSL 인증서 없음 - HTTP 설정만 적용 (SSL은 별도 실행)"
create_nginx_http_only
cp nginx/castad-http.conf /etc/nginx/sites-available/castad
ln -sf /etc/nginx/sites-available/castad /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
if nginx -t 2>/dev/null; then
systemctl reload nginx
log "HTTP nginx 설정 적용 완료"
fi
info "SSL 발급: sudo ./startserver.sh ssl"
fi
else
warn "nginx/SSL 설정은 sudo로 별도 실행하세요:"
info " sudo ./startserver.sh ssl"
fi
start_services
show_status
log "🍮 CaStAD 배포 완료!"
}
# ============================================
# 업데이트
# ============================================
update_server() {
header "서버 업데이트"
# 로컬 변경사항 체크
if ! git diff --quiet 2>/dev/null; then
warn "로컬 변경사항이 있습니다. 백업하거나 커밋하세요."
info "강제 업데이트: git checkout -- . && git pull origin main"
return 1
fi
log "Git pull 중..."
git pull origin main
# 포트 재탐색 (환경이 바뀌었을 수 있음)
find_available_ports || exit 1
# 환경변수 업데이트
check_env_file || exit 1
# 의존성 및 빌드
install_dependencies force
build_frontend
# ecosystem 재생성 (포트 설정 반영)
create_ecosystem
# 서비스 재시작
start_services
log "업데이트 완료!"
show_status
}
# ============================================
# 도움말
# ============================================
show_help() {
show_banner
echo "사용법: $0 <명령어>"
echo ""
echo "명령어:"
echo -e " ${GREEN}start${NC} 서버 전체 설치 및 시작 (첫 배포 시 사용)"
echo -e " ${GREEN}setup${NC} start + nginx + SSL 전체 설치 (sudo 필요)"
echo -e " ${GREEN}stop${NC} 서버 중지"
echo -e " ${GREEN}restart${NC} 서버 재시작"
echo -e " ${GREEN}status${NC} 서버 상태 확인"
echo -e " ${GREEN}logs${NC} 실시간 로그 보기"
echo -e " ${GREEN}update${NC} Git pull + 재빌드 + 재시작"
echo -e " ${GREEN}build${NC} 프론트엔드만 빌드"
echo -e " ${GREEN}nginx${NC} nginx 설정 설치 (sudo 필요)"
echo -e " ${GREEN}ssl${NC} SSL 인증서 발급 + 자동 설정 (sudo 필요)"
echo -e " ${GREEN}dns${NC} DNS 레코드 확인"
echo -e " ${GREEN}check${NC} 환경 및 포트 검사 (자동 탐색)"
echo ""
echo "예시:"
echo " sudo ./startserver.sh setup # 전체 설치 (권장)"
echo " ./startserver.sh start # 서버만 시작"
echo " ./startserver.sh update # 업데이트"
echo ""
echo "포트 설정 (기본값 - 충돌 시 자동 탐색):"
echo " Backend: ${BACKEND_PORT_DEFAULT} (현재: ${BACKEND_PORT})"
echo " Instagram: ${INSTAGRAM_PORT_DEFAULT} (현재: ${INSTAGRAM_PORT})"
echo ""
echo "※ 포트 충돌 시 자동으로 다음 사용 가능한 포트를 찾습니다"
echo ""
}
# ============================================
# 메인
# ============================================
case "$ACTION" in
start)
full_install
;;
setup)
# 전체 설치 (sudo로 실행 권장)
if [ "$EUID" -ne 0 ]; then
warn "setup 명령어는 sudo로 실행하는 것이 좋습니다"
info "sudo ./startserver.sh setup"
echo ""
read -p "계속하시겠습니까? (y/N) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0
fi
fi
full_install
install_ssl
show_status
echo ""
log "🍮 CaStAD 전체 설치 완료!"
info " https://${DOMAIN_PRIMARY}"
info " https://${DOMAIN_SECONDARY}"
;;
dns)
show_banner
check_dns
;;
stop)
show_banner
stop_services
;;
restart)
show_banner
find_available_ports || exit 1
check_env_file || exit 1
create_ecosystem
start_services
show_status
;;
status)
show_banner
show_status
;;
logs)
pm2 logs --lines 100
;;
update)
show_banner
update_server
;;
build)
show_banner
build_frontend
;;
nginx)
show_banner
create_nginx_config
install_nginx_config
;;
ssl)
show_banner
install_ssl
;;
check)
show_banner
check_environment
find_available_ports
;;
help|--help|-h|"")
show_help
;;
*)
error "알 수 없는 명령어: $ACTION"
show_help
exit 1
;;
esac