앞서 CI/CD를 구성하였고, Github Actions를 통해 NestJS 서버를 Docker Container에 재설치하는 것을 진행하였다.
이번엔 추가적으로 CI/CD가 실패했을 때 다시 이전 백업으로 롤백하는 기능과 시스템, Docker Container, API 등의 상태를 지속적으로 확인할 수 있는 모니터링, 주기적으로 로그들을 정리해주는 로그 정리 등의 기능을 구축해보도록 하겠다.
1. 롤백 스크립트 작성
이제 추가적으로 배포에 문제가 생겼을 때 롤백되는 스크립트를 작성하도록 해보자.
1.1. 프로젝트 경로 이동
cd /volume1/docker/NestJS_SNS
터미널에서 ssh로 NAS에 접속하여 프로젝트 경로로 이동한다.
1.2. 롤백 스크립트 작성
cat > rollback.sh << 'EOF'
#!/bin/bash
# 인수 확인 및 설정
if [ $# -lt 1 ]; then
echo "사용법: $0 <sudo_password> [ddns_host]"
echo "예시: $0 'your_password' 'xxx.synology.me'"
echo " $0 'your_password' # 기본값: xxx.synology.me 사용"
exit 1
fi
# 비밀번호를 첫 번째 인수로 받기 (deploy.sh와 일관성)
SUDO_PASSWORD="$1"
# DDNS 호스트를 두 번째 인수로 받기 (기본값: xxx.synology.me)
DDNS_HOST="${2:-xxx.synology.me}"
# 설정 변수
PROJECT_PATH="/volume1/docker/NestJS_SNS"
LOG_FILE="$PROJECT_PATH/logs/rollback.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# 역방향 프록시 DDNS 설정
API_URL="https://$DDNS_HOST"
DOCS_URL="https://$DDNS_HOST:444"
# 보존할 컨테이너들 (데이터베이스 컨테이너들)
PRESERVE_CONTAINERS="sns-dev-postgres sns-dev-redis"
# 로그 디렉토리 생성
mkdir -p "$PROJECT_PATH/logs"
echo "=====================================" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 🔄 롤백 프로세스 시작" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 🌍 DDNS 호스트: $DDNS_HOST" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 📡 API URL: $API_URL" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 📚 문서 URL: $DOCS_URL" | tee -a $LOG_FILE
echo "=====================================" | tee -a $LOG_FILE
# 프로젝트 디렉토리로 이동
cd $PROJECT_PATH || {
echo "[$TIMESTAMP] ❌ 프로젝트 디렉토리 접근 실패: $PROJECT_PATH" | tee -a $LOG_FILE
exit 1
}
# Docker 심볼릭 링크 생성 (deploy.sh와 동일)
echo "[$TIMESTAMP] 🔧 Docker 심볼릭 링크 확인" | tee -a $LOG_FILE
if [ ! -L "/usr/bin/docker" ]; then
echo "$SUDO_PASSWORD" | sudo -S ln -sf /usr/local/bin/docker /usr/bin/docker
echo "[$TIMESTAMP] ✅ Docker 심볼릭 링크 생성" | tee -a $LOG_FILE
fi
if [ ! -L "/usr/bin/docker-compose" ]; then
echo "$SUDO_PASSWORD" | sudo -S ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
echo "[$TIMESTAMP] ✅ Docker Compose 심볼릭 링크 생성" | tee -a $LOG_FILE
fi
# Docker 명령어 경로 확인 (deploy.sh와 동일)
echo "[$TIMESTAMP] 🔧 Docker 명령어 경로 확인" | tee -a $LOG_FILE
echo "[$TIMESTAMP] Docker 경로: $(which docker)" | tee -a $LOG_FILE
echo "[$TIMESTAMP] Docker Compose 경로: $(which docker-compose)" | tee -a $LOG_FILE
# 현재 서비스 상태 확인
echo "[$TIMESTAMP] 📊 현재 서비스 상태 확인" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker-compose ps | tee -a $LOG_FILE
# 백업 이미지 목록 확인
echo "[$TIMESTAMP] 💾 사용 가능한 백업 이미지 확인" | tee -a $LOG_FILE
BACKUP_IMAGES=$(echo "$SUDO_PASSWORD" | sudo -S docker images | grep "sns-dev-app.*backup-" | head -5)
if [ -z "$BACKUP_IMAGES" ]; then
echo "[$TIMESTAMP] ❌ 사용 가능한 백업 이미지가 없습니다." | tee -a $LOG_FILE
echo "[$TIMESTAMP] 💡 Git 기반 롤백 옵션:" | tee -a $LOG_FILE
# Git 커밋 히스토리 보여주기
echo "[$TIMESTAMP] 📜 최근 커밋 히스토리:" | tee -a $LOG_FILE
git log --oneline -10 | tee -a $LOG_FILE
echo "[$TIMESTAMP] 💭 수동 Git 롤백 방법:" | tee -a $LOG_FILE
echo " 1. git reset --hard <commit-hash>" | tee -a $LOG_FILE
echo " 2. ./deploy.sh <password> <ddns_host>" | tee -a $LOG_FILE
echo "[$TIMESTAMP] ⚠️ 백업 이미지 생성을 위해 deploy.sh를 다시 실행하세요." | tee -a $LOG_FILE
exit 1
fi
echo "[$TIMESTAMP] 📋 백업 이미지 목록:" | tee -a $LOG_FILE
echo "$BACKUP_IMAGES" | tee -a $LOG_FILE
# 사용자에게 백업 선택 옵션 제공
echo "[$TIMESTAMP] 🎯 백업 이미지 선택 옵션:" | tee -a $LOG_FILE
echo "$BACKUP_IMAGES" | nl -w2 -s'. ' | tee -a $LOG_FILE
# 가장 최근 백업을 기본값으로 사용
LATEST_BACKUP=$(echo "$BACKUP_IMAGES" | head -1 | awk '{print $1":"$2}')
BACKUP_TAG=$(echo "$BACKUP_IMAGES" | head -1 | awk '{print $2}')
echo "[$TIMESTAMP] 🎯 선택된 백업: $LATEST_BACKUP" | tee -a $LOG_FILE
# 현재 상태 임시 백업 (롤백 전 안전장치)
echo "[$TIMESTAMP] 💾 현재 상태 임시 백업 (안전장치)" | tee -a $LOG_FILE
ROLLBACK_BACKUP="rollback-backup-$(date +%Y%m%d-%H%M%S)"
if echo "$SUDO_PASSWORD" | sudo -S docker tag sns-dev-app:latest sns-dev-app:$ROLLBACK_BACKUP 2>/dev/null; then
echo "[$TIMESTAMP] ✅ 현재 이미지 백업 성공: $ROLLBACK_BACKUP" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ⚠️ 현재 이미지 백업 실패 (계속 진행)" | tee -a $LOG_FILE
fi
# 보존할 컨테이너 상태 확인
echo "[$TIMESTAMP] 🛡️ 보존할 컨테이너 상태 확인" | tee -a $LOG_FILE
for container in $PRESERVE_CONTAINERS; do
if echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then
CONTAINER_STATUS=$(echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^${container}" | cut -f2)
echo "[$TIMESTAMP] 🛡️ 보존된 컨테이너: $container - $CONTAINER_STATUS" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ⚠️ 컨테이너 없음: $container" | tee -a $LOG_FILE
fi
done
# 애플리케이션 컨테이너만 중지
echo "[$TIMESTAMP] 🛑 애플리케이션 컨테이너만 중지 (DB 보존)" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker-compose stop app monitor | tee -a $LOG_FILE
# 기존 애플리케이션 컨테이너 제거
CLEANUP_CONTAINERS="sns-dev-app sns-dev-monitor"
for container in $CLEANUP_CONTAINERS; do
if echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then
echo "[$TIMESTAMP] 🗑️ 기존 컨테이너 제거: $container" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker rm -f "$container" | tee -a $LOG_FILE
fi
done
# 백업 이미지로 복원
echo "[$TIMESTAMP] 🔄 백업 이미지로 복원: $BACKUP_TAG" | tee -a $LOG_FILE
if echo "$SUDO_PASSWORD" | sudo -S docker tag sns-dev-app:$BACKUP_TAG sns-dev-app:latest; then
echo "[$TIMESTAMP] ✅ 이미지 태그 변경 성공" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ❌ 이미지 태그 변경 실패" | tee -a $LOG_FILE
exit 1
fi
# 서비스 시작 (DB 컨테이너는 유지된 상태)
echo "[$TIMESTAMP] 🚀 서비스 시작" | tee -a $LOG_FILE
if echo "$SUDO_PASSWORD" | sudo -S docker-compose up -d; then
echo "[$TIMESTAMP] ✅ 서비스 시작 성공" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ❌ 서비스 시작 실패" | tee -a $LOG_FILE
exit 1
fi
# 서비스 시작 대기 (HTTPS 기반으로 변경)
echo "[$TIMESTAMP] ⏳ 서비스 시작 대기 중..." | tee -a $LOG_FILE
for i in {1..12}; do # 최대 60초 대기 (5초씩 12번)
# HTTPS 헬스체크로 변경 (SSL 검증 무시)
if curl -k -f "$API_URL/health" > /dev/null 2>&1; then
echo "[$TIMESTAMP] ✅ 서비스 시작 완료 ($((i*5))초 소요)" | tee -a $LOG_FILE
break
else
echo "[$TIMESTAMP] ⏳ 서비스 시작 대기 중... ($((i*5))초)" | tee -a $LOG_FILE
# 5번째 시도 후부터 컨테이너 로그 확인
if [ $i -eq 5 ]; then
echo "[$TIMESTAMP] 🔍 애플리케이션 컨테이너 로그 확인 (최근 10줄)" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker logs --tail=10 sns-dev-app 2>&1 | tee -a $LOG_FILE || echo "앱 컨테이너 로그를 가져올 수 없습니다." | tee -a $LOG_FILE
fi
sleep 5
fi
if [ $i -eq 12 ]; then
echo "[$TIMESTAMP] ⚠️ 서비스 시작 시간이 오래 걸리고 있습니다." | tee -a $LOG_FILE
# 대체 헬스체크: 내부 네트워크로 시도
echo "[$TIMESTAMP] 🔄 내부 네트워크 헬스체크 시도" | tee -a $LOG_FILE
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
echo "[$TIMESTAMP] ✅ 내부 네트워크 헬스체크 통과 (역방향 프록시 설정 확인 필요)" | tee -a $LOG_FILE
break
else
echo "[$TIMESTAMP] ❌ 내부 네트워크 헬스체크도 실패" | tee -a $LOG_FILE
# 최종 디버깅 정보
echo "[$TIMESTAMP] 🔍 최종 디버깅 정보" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 컨테이너 상세 상태:" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | tee -a $LOG_FILE
echo "[$TIMESTAMP] ❌ 롤백 후 서비스 시작 타임아웃" | tee -a $LOG_FILE
exit 1
fi
fi
done
# 종합적인 헬스체크 수행
echo "[$TIMESTAMP] 🔍 롤백 후 종합 헬스체크" | tee -a $LOG_FILE
# HTTPS API 헬스체크
echo "[$TIMESTAMP] 🔍 NestJS API 헬스체크 (HTTPS)" | tee -a $LOG_FILE
if curl -k -f "$API_URL/health" > /dev/null 2>&1; then
echo "[$TIMESTAMP] ✅ NestJS API HTTPS 헬스체크 통과" | tee -a $LOG_FILE
# API 응답 내용도 확인
API_RESPONSE=$(curl -k -s "$API_URL/health")
echo "[$TIMESTAMP] 🧪 API 응답: $API_RESPONSE" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ❌ NestJS API HTTPS 헬스체크 실패" | tee -a $LOG_FILE
# 대체 헬스체크: 내부 네트워크로 시도
echo "[$TIMESTAMP] 🔄 내부 네트워크 헬스체크 시도" | tee -a $LOG_FILE
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
echo "[$TIMESTAMP] ✅ 내부 네트워크 헬스체크 통과 (역방향 프록시 설정 확인 필요)" | tee -a $LOG_FILE
API_RESPONSE=$(curl -s http://localhost:3000/health)
echo "[$TIMESTAMP] 🧪 API 응답: $API_RESPONSE" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ❌ 내부 네트워크 헬스체크도 실패" | tee -a $LOG_FILE
exit 1
fi
fi
# Swagger UI 접근 확인
echo "[$TIMESTAMP] 🔍 Swagger UI 접근 확인" | tee -a $LOG_FILE
SWAGGER_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$API_URL/api" 2>/dev/null)
if [ "$SWAGGER_STATUS" = "200" ] || [ "$SWAGGER_STATUS" = "401" ]; then
echo "[$TIMESTAMP] ✅ Swagger UI 접근 가능 (HTTP $SWAGGER_STATUS)" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ⚠️ Swagger UI 접근 상태: HTTP $SWAGGER_STATUS" | tee -a $LOG_FILE
fi
# GraphQL 접근 확인
echo "[$TIMESTAMP] 🔍 GraphQL 접근 확인" | tee -a $LOG_FILE
GRAPHQL_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$API_URL/graphql" 2>/dev/null)
if [ "$GRAPHQL_STATUS" = "200" ] || [ "$GRAPHQL_STATUS" = "400" ] || [ "$GRAPHQL_STATUS" = "405" ]; then
echo "[$TIMESTAMP] ✅ GraphQL 엔드포인트 접근 가능 (HTTP $GRAPHQL_STATUS)" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ⚠️ GraphQL 엔드포인트 상태: HTTP $GRAPHQL_STATUS" | tee -a $LOG_FILE
fi
# Compodoc 문서 접근 확인
echo "[$TIMESTAMP] 🔍 Compodoc 문서 접근 확인" | tee -a $LOG_FILE
DOCS_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$DOCS_URL/" 2>/dev/null)
if [ "$DOCS_STATUS" = "200" ]; then
echo "[$TIMESTAMP] ✅ Compodoc 문서 접근 가능 (HTTP $DOCS_STATUS)" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ⚠️ Compodoc 문서 상태: HTTP $DOCS_STATUS" | tee -a $LOG_FILE
fi
# 데이터베이스 헬스체크
echo "[$TIMESTAMP] 🔍 데이터베이스 헬스체크" | tee -a $LOG_FILE
if echo "$SUDO_PASSWORD" | sudo -S docker exec sns-dev-postgres pg_isready -U postgres > /dev/null 2>&1; then
echo "[$TIMESTAMP] ✅ PostgreSQL 헬스체크 통과" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ❌ PostgreSQL 헬스체크 실패" | tee -a $LOG_FILE
fi
# Redis 헬스체크
if echo "$SUDO_PASSWORD" | sudo -S docker exec sns-dev-redis redis-cli ping > /dev/null 2>&1; then
echo "[$TIMESTAMP] ✅ Redis 헬스체크 통과" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ❌ Redis 헬스체크 실패" | tee -a $LOG_FILE
fi
# 모니터링 컨테이너 상태 확인
echo "[$TIMESTAMP] 🔍 모니터링 컨테이너 상태 확인" | tee -a $LOG_FILE
MONITOR_STATUS=$(echo "$SUDO_PASSWORD" | sudo -S docker ps --format "{{.Names}}\t{{.Status}}" | grep "sns-dev-monitor" || echo "없음")
if [ "$MONITOR_STATUS" != "없음" ]; then
echo "[$TIMESTAMP] ✅ 모니터링 컨테이너: $MONITOR_STATUS" | tee -a $LOG_FILE
else
echo "[$TIMESTAMP] ⚠️ 모니터링 컨테이너가 실행되지 않았습니다." | tee -a $LOG_FILE
fi
# 최종 컨테이너 상태 확인
echo "[$TIMESTAMP] 📋 롤백 후 최종 컨테이너 상태" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker-compose ps | tee -a $LOG_FILE
# 롤백 정보 출력 (HTTPS 우선)
echo "[$TIMESTAMP] 🌐 롤백된 서비스 접근 정보 (역방향 프록시)" | tee -a $LOG_FILE
echo " 🔒 HTTPS 접근 (권장):" | tee -a $LOG_FILE
echo " 📡 NestJS API: $API_URL" | tee -a $LOG_FILE
echo " 📋 Swagger UI: $API_URL/api" | tee -a $LOG_FILE
echo " 🚀 GraphQL: $API_URL/graphql" | tee -a $LOG_FILE
echo " 🔍 Health Check: $API_URL/health" | tee -a $LOG_FILE
echo " 📚 Compodoc: $DOCS_URL" | tee -a $LOG_FILE
echo " 📊 모니터링:" | tee -a $LOG_FILE
echo " 🔍 모니터링 로그: docker exec sns-dev-monitor tail -f /monitor/logs/monitor.log" | tee -a $LOG_FILE
echo " 📈 실시간 모니터링: docker exec sns-dev-monitor ./monitor.sh" | tee -a $LOG_FILE
# Git 상태 정보
echo "[$TIMESTAMP] 📊 현재 Git 상태" | tee -a $LOG_FILE
git log --oneline -3 | tee -a $LOG_FILE
echo "=====================================" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 🎉 롤백 프로세스 완료" | tee -a $LOG_FILE
echo "=====================================" | tee -a $LOG_FILE
# 롤백 성공 알림
echo "[$TIMESTAMP] 📢 롤백 완료 알림" | tee -a $LOG_FILE
echo " - 이전 백업으로 서비스가 복원되었습니다." | tee -a $LOG_FILE
echo " - 사용된 백업 이미지: $BACKUP_TAG" | tee -a $LOG_FILE
echo " - 안전장치 백업: $ROLLBACK_BACKUP" | tee -a $LOG_FILE
echo " - 데이터베이스 컨테이너는 보존되었습니다." | tee -a $LOG_FILE
echo " - DDNS 호스트: $DDNS_HOST" | tee -a $LOG_FILE
echo " - 현재 서비스가 정상 동작하는지 확인하세요." | tee -a $LOG_FILE
# 명시적으로 성공 상태 반환
echo "[$TIMESTAMP] 📤 롤백 성공 상태 반환 (Exit Code: 0)" | tee -a $LOG_FILE
exit 0
EOF
이전 백업이 있는 경우 이전 백업으로 복원하게 된다.
1.3. 권한 설정
chmod +x rollback.sh
2. 모니터링 스크립트 작성
2.1. 프로젝트 경로 이동
cd /volume1/docker/NestJS_SNS
2.2. Dockerfile.monitor 작성
cat > Dockerfile.monitor << 'EOF'
FROM alpine:3.19
RUN apk add --no-cache \
bash \
curl \
git \
docker-cli \
findutils \
coreutils \
gzip \
procps
# 작업 디렉토리 설정
WORKDIR /monitor
# 스크립트 복사
COPY monitor.sh /monitor/monitor.sh
COPY log-rotate.sh /monitor/log-rotate.sh
# 실행 권한 부여
RUN chmod +x /monitor/monitor.sh /monitor/log-rotate.sh
# 기본 명령어
CMD ["sh", "-c", "while true; do ./monitor.sh; sleep ${MONITOR_INTERVAL:-60}; done"]
EOF
docker-compose.yaml이 실행되어 컨테이너에서 Dockerfile.monitor 파일이 실행된다.
Dockerfile.monitor는 run_monitor.sh 파일을 실행한다.
2.3. run_monitor.sh 작성
cat > run_monitor.sh << 'EOF'
#!/bin/bash
echo "🔍 Docker 모니터링 컨테이너 시작"
echo "📅 시작 시간: $(date '+%Y-%m-%d %H:%M:%S')"
echo "⏰ 1분마다 모니터링 실행"
echo "🐳 컨테이너: $(hostname)"
echo "========================================="
# 초기 대기 (다른 서비스들이 시작될 시간)
echo "⏳ 초기 대기 중... (60초)"
sleep 60
# 초기 실행
echo "🔄 첫 번째 모니터링 실행"
/monitor/monitor.sh
# 1분마다 반복 실행
while true; do
sleep 60
echo ""
echo "🔄 모니터링 갱신: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="
/monitor/monitor.sh
done
EOF
//권한 설정
chmod +x run_monitor.sh
run_monitor.sh 스크립트는 컨테이너에서 주기적으로 반복적으로 실행되며, monitor.sh 파일을 호출하여 상태를 보여준다.
2.4. monitor.sh 작성
cat > monitor.sh << 'EOF'
#!/bin/bash
# 환경변수에서 설정 가져오기 (DDNS 중심)
PROJECT_PATH="${PROJECT_PATH:-/project}"
DDNS_HOST="${DDNS_HOST:-xxx.synology.me}"
MONITOR_INTERVAL="${MONITOR_INTERVAL:-60}"
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-7}"
HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-10}"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
LOG_FILE="/monitor/logs/monitor.log"
# DDNS 기반 URL 구성
API_URL="https://$DDNS_HOST"
DOCS_URL="https://$DDNS_HOST:444"
# 로그 디렉토리 생성
mkdir -p "/monitor/logs"
# Git 설정 초기화 (환경변수 기반)
setup_git_config() {
local git_user_name="${GIT_USER_NAME:-CI/CD Monitor}"
local git_user_email="${GIT_USER_EMAIL:-monitor@cicd.local}"
local git_safe_dir="${GIT_SAFE_DIRECTORY:-/project}"
# Git 설정이 없으면 초기화
if ! git config --global user.name >/dev/null 2>&1; then
git config --global user.name "$git_user_name"
echo "🔧 Git user.name 설정: $git_user_name" >> $LOG_FILE
fi
if ! git config --global user.email >/dev/null 2>&1; then
git config --global user.email "$git_user_email"
echo "🔧 Git user.email 설정: $git_user_email" >> $LOG_FILE
fi
# Safe directory 설정
if [ -d "$git_safe_dir/.git" ]; then
git config --global --add safe.directory "$git_safe_dir" 2>/dev/null || true
echo "🔧 Git safe directory 설정: $git_safe_dir" >> $LOG_FILE
fi
}
# Git 설정 실행
setup_git_config
# 자동 로그 정리 (하루에 한 번, 새벽 3시경 또는 수동 실행시)
if [ $(date +%H:%M) = "03:00" ] || [ ! -f "/monitor/.last_cleanup" ] || [ $(find "/monitor/.last_cleanup" -mtime +1 2>/dev/null | wc -l) -gt 0 ]; then
echo "🧹 자동 로그 정리 시작..."
if [ -x "/monitor/log-rotate.sh" ]; then
/monitor/log-rotate.sh
touch "/monitor/.last_cleanup"
echo "✅ 자동 로그 정리 완료"
else
echo "⚠️ log-rotate.sh 스크립트를 찾을 수 없습니다."
fi
fi
# 헬퍼 함수들 (HEALTH_CHECK_TIMEOUT 사용)
check_https_service() {
local service_name="$1"
local url="$2"
local timeout="${3:-$HEALTH_CHECK_TIMEOUT}"
local status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
if [ "$status" = "200" ]; then
echo "✅ $service_name: 정상 (HTTPS $status)"
return 0
else
echo "❌ $service_name: 오류 (HTTPS $status)"
# 상세 디버깅 정보 추가
if [ "$status" = "401" ]; then
echo " └─ 💡 401 Unauthorized: 인증이 필요하거나 경로가 잘못되었을 수 있습니다"
elif [ "$status" = "404" ]; then
echo " └─ 💡 404 Not Found: 경로가 존재하지 않습니다"
elif [ "$status" = "000" ]; then
echo " └─ 💡 연결 실패: 서버에 연결할 수 없습니다"
fi
return 1
fi
}
# Swagger UI 전용 체크 함수 (401도 정상으로 처리)
check_swagger_ui() {
local service_name="$1"
local url="$2"
local timeout="${3:-$HEALTH_CHECK_TIMEOUT}"
local status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
local response=$(curl -s --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
# 200 또는 401을 정상적인 Swagger UI 응답으로 처리
if [ "$status" = "200" ] || [ "$status" = "401" ]; then
# HTML 응답에서 Swagger 관련 키워드 확인
if echo "$response" | grep -qi "swagger\|openapi\|unauthorized\|api.*documentation"; then
echo "✅ $service_name: 정상 (HTTPS $status) - Swagger UI 확인됨"
if [ "$status" = "401" ]; then
echo " └─ 💡 인증이 필요한 Swagger UI입니다 (브라우저 접근 권장)"
fi
return 0
elif [ "$status" = "200" ]; then
echo "✅ $service_name: 정상 (HTTPS $status) - HTML 응답 확인"
return 0
else
echo "⚠️ $service_name: 응답 확인 필요 (HTTPS $status)"
echo " └─ 💡 HTML 응답이 있지만 Swagger 키워드를 찾을 수 없습니다"
return 0
fi
else
echo "❌ $service_name: 오류 (HTTPS $status)"
if [ "$status" = "404" ]; then
echo " └─ 💡 404 Not Found: 경로가 존재하지 않습니다"
elif [ "$status" = "000" ]; then
echo " └─ 💡 연결 실패: 서버에 연결할 수 없습니다"
fi
return 1
fi
}
# GraphQL 전용 체크 함수 (400, 405도 정상으로 처리)
check_graphql() {
local service_name="$1"
local url="$2"
local timeout="${3:-$HEALTH_CHECK_TIMEOUT}"
local status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
local response=$(curl -s --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
# 200, 400, 405를 정상적인 GraphQL 응답으로 처리
if [ "$status" = "200" ] || [ "$status" = "400" ] || [ "$status" = "405" ]; then
# 응답에서 GraphQL 관련 키워드 확인
if echo "$response" | grep -qi "graphql\|playground\|query\|mutation\|subscription\|schema"; then
echo "✅ $service_name: 정상 (HTTPS $status) - GraphQL 확인됨"
if [ "$status" = "400" ]; then
echo " └─ 💡 400 Bad Request: GraphQL 쿼리가 필요합니다 (정상 응답)"
elif [ "$status" = "405" ]; then
echo " └─ 💡 405 Method Not Allowed: POST 요청이 필요합니다 (정상 응답)"
fi
return 0
elif echo "$response" | grep -qi "Cannot GET\|Method.*not.*allowed"; then
echo "✅ $service_name: 정상 (HTTPS $status) - GraphQL 엔드포인트 확인됨"
echo " └─ 💡 GET 요청 불가 (GraphQL은 POST 필요)"
return 0
elif [ "$status" = "200" ]; then
echo "✅ $service_name: 정상 (HTTPS $status) - 응답 확인"
return 0
else
echo "⚠️ $service_name: 응답 확인 필요 (HTTPS $status)"
echo " └─ 💡 응답이 있지만 GraphQL 키워드를 찾을 수 없습니다"
return 0
fi
else
echo "❌ $service_name: 오류 (HTTPS $status)"
if [ "$status" = "404" ]; then
echo " └─ 💡 404 Not Found: GraphQL 경로가 존재하지 않습니다"
elif [ "$status" = "000" ]; then
echo " └─ 💡 연결 실패: 서버에 연결할 수 없습니다"
fi
return 1
fi
}
# 컨테이너 내부 포트 체크 함수
check_container_port() {
local port="$1"
local service="$2"
local container="$3"
local timeout="${4:-$HEALTH_CHECK_TIMEOUT}"
case $port in
3000)
if curl -s --connect-timeout $timeout "http://$container:3000/health" >/dev/null 2>&1; then
echo " ✅ 포트 $port ($service): 활성화"
else
echo " ❌ 포트 $port ($service): 비활성화"
fi
;;
8080)
if curl -s --connect-timeout $timeout "http://$container:8080/" >/dev/null 2>&1; then
echo " ✅ 포트 $port ($service): 활성화"
else
echo " ❌ 포트 $port ($service): 비활성화"
fi
;;
5432)
if docker exec "$container" pg_isready -U postgres >/dev/null 2>&1; then
echo " ✅ 포트 $port ($service): 활성화"
else
echo " ❌ 포트 $port ($service): 비활성화"
fi
;;
6379)
if docker exec "$container" redis-cli ping >/dev/null 2>&1; then
echo " ✅ 포트 $port ($service): 활성화"
else
echo " ❌ 포트 $port ($service): 비활성화"
fi
;;
*)
echo " ❌ 포트 $port ($service): 비활성화"
;;
esac
}
# 자동 로그 정리 (하루에 한 번, 새벽 3시경)
if [ $(date +%H:%M) = "03:00" ] || [ ! -f "/monitor/.last_cleanup" ] || [ $(find "/monitor/.last_cleanup" -mtime +1 2>/dev/null | wc -l) -gt 0 ]; then
cleanup_old_logs
touch "/monitor/.last_cleanup"
fi
# 모니터링 시작 로그
echo "[$TIMESTAMP] 🔍 컨테이너 모니터링 시작 (간격: ${MONITOR_INTERVAL}초, 타임아웃: ${HEALTH_CHECK_TIMEOUT}초, 로그보관: ${LOG_RETENTION_DAYS}일)" | tee -a $LOG_FILE
echo "======================================"
echo "🔍 NAS CI/CD 모니터링 대시보드 (Container)"
echo "📅 시간: $TIMESTAMP"
echo "🌍 DDNS: $DDNS_HOST"
echo "🔒 API URL: $API_URL"
echo "📚 문서 URL: $DOCS_URL"
echo "🐳 모니터링 컨테이너: $(hostname)"
echo "⚙️ 설정: 간격 ${MONITOR_INTERVAL}초 | 타임아웃 ${HEALTH_CHECK_TIMEOUT}초 | 로그보관 ${LOG_RETENTION_DAYS}일"
echo "======================================"
# 컨테이너 시스템 정보
echo ""
echo "🖥️ 컨테이너 시스템 정보:"
echo " - 컨테이너 ID: $(hostname)"
echo " - 현재 사용자: $(whoami)"
echo " - 시스템 시간: $(date '+%Y-%m-%d %H:%M:%S %Z')"
# Docker 컨테이너 상태 확인
echo ""
echo "🐳 Docker 컨테이너 상태:"
# 모든 SNS 관련 컨테이너 확인
SNS_CONTAINERS=$(docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "sns-dev" 2>/dev/null)
if [ -n "$SNS_CONTAINERS" ]; then
echo "$SNS_CONTAINERS" | while IFS=$'\t' read -r name status; do
if echo "$status" | grep -q "Up"; then
echo " ✅ $name: $status"
else
echo " ❌ $name: $status"
fi
done
else
echo " ⚠️ SNS 관련 컨테이너를 찾을 수 없습니다."
echo " 💡 전체 컨테이너 확인:"
docker ps -a --format " {{.Names}}\t{{.Status}}" 2>/dev/null | head -5
fi
# 컨테이너 네트워크 내부 서비스 체크
echo ""
echo "🔍 서비스 헬스체크 (컨테이너 네트워크 내부):"
# API 서비스 체크 (컨테이너명으로 접근)
API_STATUS_INTERNAL=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT "http://sns-dev-app:3000/health" 2>/dev/null)
if [ "$API_STATUS_INTERNAL" = "200" ]; then
API_RESPONSE=$(curl -s --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT "http://sns-dev-app:3000/health" 2>/dev/null)
echo " ✅ API 서버 (컨테이너 내부): 정상 (HTTP $API_STATUS_INTERNAL)"
echo " └─ 응답: $API_RESPONSE"
else
echo " ❌ API 서버 (컨테이너 내부): 오류 (HTTP $API_STATUS_INTERNAL)"
fi
# Compodoc 문서 서비스 체크
DOCS_STATUS_INTERNAL=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT "http://sns-dev-app:8080/" 2>/dev/null)
if [ "$DOCS_STATUS_INTERNAL" = "200" ]; then
echo " ✅ Compodoc 문서 (컨테이너 내부): 정상 (HTTP $DOCS_STATUS_INTERNAL)"
else
echo " ❌ Compodoc 문서 (컨테이너 내부): 오류 (HTTP $DOCS_STATUS_INTERNAL)"
fi
# HTTPS 접근 체크 (역방향 프록시를 통한 외부 접근)
echo ""
echo "🔒 HTTPS 접근 상태 (역방향 프록시):"
# HTTPS API 헬스체크
if check_https_service "HTTPS API 헬스체크" "$API_URL/health"; then
echo " └─ 주소: $API_URL/health"
fi
# HTTPS Swagger UI 체크 (401도 정상으로 처리)
echo ""
echo "🔍 Swagger UI 경로 체크 (401 허용):"
SWAGGER_PATHS=("/api" "/api-docs" "/swagger" "/docs")
SWAGGER_FOUND=false
for path in "${SWAGGER_PATHS[@]}"; do
if check_swagger_ui "HTTPS Swagger UI ($path)" "$API_URL$path"; then
echo " └─ 주소: $API_URL$path"
SWAGGER_FOUND=true
SWAGGER_WORKING_PATH="$path"
break
fi
done
if [ "$SWAGGER_FOUND" = false ]; then
echo " ⚠️ 모든 Swagger 경로에서 접근 실패. 설정을 확인해주세요."
# 대체 경로들의 상태코드 표시
echo " 📋 경로별 상태코드:"
for path in "${SWAGGER_PATHS[@]}"; do
status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL$path" 2>/dev/null)
echo " - $path: HTTP $status"
done
fi
# HTTPS GraphQL 체크 (400, 405도 정상으로 처리)
echo ""
if check_graphql "HTTPS GraphQL" "$API_URL/graphql"; then
echo " └─ 주소: $API_URL/graphql"
fi
# HTTPS Compodoc 문서 체크 (444 포트)
if check_https_service "HTTPS Compodoc 문서" "$DOCS_URL/"; then
echo " └─ 주소: $DOCS_URL"
fi
# 데이터베이스 상태 확인
echo ""
echo "🗄️ 데이터베이스 상태:"
# PostgreSQL 체크
if docker exec sns-dev-postgres pg_isready -U postgres >/dev/null 2>&1; then
echo " ✅ PostgreSQL: 정상"
# 연결 수 확인
DB_CONNECTIONS=$(docker exec sns-dev-postgres psql -U postgres -t -c "SELECT count(*) FROM pg_stat_activity;" 2>/dev/null | xargs || echo "확인불가")
echo " └─ 활성 연결: $DB_CONNECTIONS개"
# 데이터베이스 크기 확인
DB_SIZE=$(docker exec sns-dev-postgres psql -U postgres -t -c "SELECT pg_size_pretty(pg_database_size('postgres'));" 2>/dev/null | xargs || echo "확인불가")
echo " └─ DB 크기: $DB_SIZE"
else
echo " ❌ PostgreSQL: 오류 또는 연결 불가"
fi
# Redis 체크
if docker exec sns-dev-redis redis-cli ping >/dev/null 2>&1; then
echo " ✅ Redis: 정상"
# Redis 메모리 사용량
REDIS_MEMORY=$(docker exec sns-dev-redis redis-cli info memory 2>/dev/null | grep "used_memory_human" | cut -d: -f2 | tr -d '\r' || echo "확인불가")
echo " └─ 메모리 사용: $REDIS_MEMORY"
# Redis 키 개수
REDIS_KEYS=$(docker exec sns-dev-redis redis-cli dbsize 2>/dev/null || echo "확인불가")
echo " └─ 저장된 키: $REDIS_KEYS개"
else
echo " ❌ Redis: 오류 또는 연결 불가"
fi
# 컨테이너별 포트 확인 (HEALTH_CHECK_TIMEOUT 사용)
echo ""
echo "🔌 컨테이너별 서비스 포트:"
check_container_port 3000 "API" "sns-dev-app" $HEALTH_CHECK_TIMEOUT
check_container_port 8080 "문서" "sns-dev-app" $HEALTH_CHECK_TIMEOUT
check_container_port 5432 "PostgreSQL" "sns-dev-postgres"
check_container_port 6379 "Redis" "sns-dev-redis"
# Git 상태 (프로젝트 디렉토리에서)
echo ""
echo "📊 Git 저장소 상태:"
if [ -d "$PROJECT_PATH" ]; then
cd "$PROJECT_PATH" 2>/dev/null || {
echo " ❌ 프로젝트 디렉토리 접근 실패: $PROJECT_PATH"
return
}
if [ -d ".git" ]; then
echo " ✅ Git 저장소 확인됨"
# Git 명령어 실행 - safe directory 설정이 적용된 상태
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "확인 불가")
echo " - 현재 브랜치: $CURRENT_BRANCH"
LATEST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "확인 불가")
echo " - 최근 커밋: $LATEST_COMMIT"
UNCOMMITTED_CHANGES=$(git status --porcelain 2>/dev/null | wc -l)
echo " - 미커밋 변경사항: ${UNCOMMITTED_CHANGES}개"
# 원격 저장소와의 차이 확인
if git remote >/dev/null 2>&1; then
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "확인 불가")
echo " - 원격 저장소: $REMOTE_URL"
# fetch 시도
if git fetch >/dev/null 2>&1; then
BEHIND_COUNT=$(git rev-list --count HEAD..origin/main 2>/dev/null || echo "0")
AHEAD_COUNT=$(git rev-list --count origin/main..HEAD 2>/dev/null || echo "0")
echo " - 원격 대비: $BEHIND_COUNT개 뒤처짐, $AHEAD_COUNT개 앞섬"
else
echo " - 원격 저장소 fetch 실패"
fi
else
echo " - 원격 저장소: 설정되지 않음"
fi
else
echo " ❌ Git 저장소가 아닙니다"
fi
else
echo " ❌ 프로젝트 디렉토리가 존재하지 않습니다: $PROJECT_PATH"
fi
# 최근 로그 파일들
echo ""
echo "📝 최근 로그 상태:"
# 배포 로그
if [ -f "$PROJECT_PATH/logs/deploy.log" ]; then
DEPLOY_LOG_SIZE=$(du -h "$PROJECT_PATH/logs/deploy.log" | cut -f1)
DEPLOY_LOG_LINES=$(wc -l < "$PROJECT_PATH/logs/deploy.log")
echo " - 배포 로그: ${DEPLOY_LOG_SIZE} (${DEPLOY_LOG_LINES}줄)"
echo " └─ 마지막 5줄:"
tail -5 "$PROJECT_PATH/logs/deploy.log" | sed 's/^/ /'
else
echo " - 배포 로그: 없음"
fi
# 롤백 로그
if [ -f "$PROJECT_PATH/logs/rollback.log" ]; then
ROLLBACK_LOG_SIZE=$(du -h "$PROJECT_PATH/logs/rollback.log" | cut -f1)
echo " - 롤백 로그: $ROLLBACK_LOG_SIZE"
else
echo " - 롤백 로그: 없음"
fi
# 모니터링 로그 상태
if [ -f "$LOG_FILE" ]; then
MONITOR_LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
MONITOR_LOG_LINES=$(wc -l < "$LOG_FILE")
echo " - 모니터링 로그: ${MONITOR_LOG_SIZE} (${MONITOR_LOG_LINES}줄)"
else
echo " - 모니터링 로그: 생성 중..."
fi
# 애플리케이션 에러 로그
echo ""
echo "🚨 최근 에러 로그:"
ERROR_LOGS=$(docker logs sns-dev-app --tail=20 2>/dev/null | grep -i "error\|exception\|fatal" | tail -3)
if [ -n "$ERROR_LOGS" ]; then
echo "$ERROR_LOGS" | sed 's/^/ /'
else
echo " ✅ 최근 에러가 없습니다."
fi
# 리소스 사용량 상세
echo ""
echo "💾 저장공간 사용량:"
PROJECT_SIZE=$(du -sh "$PROJECT_PATH" 2>/dev/null | awk '{print $1}' || echo "확인 불가")
echo " - 프로젝트 디렉토리: $PROJECT_SIZE ($PROJECT_PATH)"
# Docker 이미지 및 볼륨 정보
DOCKER_IMAGES_COUNT=$(docker images --format '{{.Repository}}' | grep -E '(sns|2_cf_sns)' | wc -l 2>/dev/null || echo "0")
DOCKER_VOLUMES_COUNT=$(docker volume ls | grep -E '(pnpm|redis)' | wc -l 2>/dev/null || echo "0")
echo " - Docker 이미지: ${DOCKER_IMAGES_COUNT}개"
echo " - Docker 볼륨: ${DOCKER_VOLUMES_COUNT}개"
# 서비스 접근 링크 (DDNS 기반)
echo ""
echo "🌐 서비스 접근 링크 (DDNS 기반):"
echo ""
echo " 🔒 HTTPS 접근 (역방향 프록시):"
echo " 📡 API 헬스체크: $API_URL/health"
if [ "$SWAGGER_FOUND" = true ]; then
echo " 📋 Swagger UI: $API_URL$SWAGGER_WORKING_PATH ✅"
else
echo " 📋 Swagger UI: $API_URL/api (확인 필요)"
fi
echo " 🚀 GraphQL: $API_URL/graphql"
echo " 📚 Compodoc 문서: $DOCS_URL"
# 역방향 프록시 상태 요약
echo ""
echo "🔀 역방향 프록시 구성:"
echo " - HTTPS → API (3000): $API_URL/* → http://sns-dev-app:3000/*"
echo " - HTTPS → 문서 (444): $DOCS_URL/* → http://sns-dev-app:8080/*"
echo " - SSL 인증서: Synology DSM 관리"
echo " - Swagger UI 인증: 401 상태는 정상 (브라우저 접근 권장)"
echo " - GraphQL 응답: 400/405 상태는 정상 (POST 요청 필요)"
# 상태 요약
echo ""
echo "📊 상태 요약:"
OVERALL_STATUS="✅ 정상"
# HTTPS 접근 상태 확인 (HEALTH_CHECK_TIMEOUT 사용)
HTTPS_API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL/health" 2>/dev/null)
HTTPS_SWAGGER_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL/api" 2>/dev/null)
HTTPS_GRAPHQL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL/graphql" 2>/dev/null)
HTTPS_DOCS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$DOCS_URL/" 2>/dev/null)
if [ "$API_STATUS_INTERNAL" != "200" ]; then
OVERALL_STATUS="⚠️ API 서버 문제"
elif [ "$DOCS_STATUS_INTERNAL" != "200" ]; then
OVERALL_STATUS="⚠️ 문서 서버 문제"
elif [ "$HTTPS_API_STATUS" != "200" ]; then
OVERALL_STATUS="⚠️ HTTPS API 프록시 문제"
elif [ "$HTTPS_DOCS_STATUS" != "200" ]; then
OVERALL_STATUS="⚠️ HTTPS 문서 프록시 문제"
fi
echo " - 전체 상태: $OVERALL_STATUS"
echo " - API 상태 (내부): $([ "$API_STATUS_INTERNAL" = "200" ] && echo "정상" || echo "문제")"
echo " - 문서 상태 (내부): $([ "$DOCS_STATUS_INTERNAL" = "200" ] && echo "정상" || echo "문제")"
echo " - HTTPS API 상태: $([ "$HTTPS_API_STATUS" = "200" ] && echo "정상" || echo "문제")"
echo " - HTTPS Swagger 상태: $([ "$HTTPS_SWAGGER_STATUS" = "200" ] || [ "$HTTPS_SWAGGER_STATUS" = "401" ] && echo "정상" || echo "문제")"
echo " - HTTPS GraphQL 상태: $([ "$HTTPS_GRAPHQL_STATUS" = "200" ] || [ "$HTTPS_GRAPHQL_STATUS" = "400" ] || [ "$HTTPS_GRAPHQL_STATUS" = "405" ] && echo "정상" || echo "문제")"
echo " - HTTPS 문서 상태: $([ "$HTTPS_DOCS_STATUS" = "200" ] && echo "정상" || echo "문제")"
echo " - 마지막 체크: $TIMESTAMP"
echo " - 다음 체크: $(date -d "+${MONITOR_INTERVAL} seconds" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')"
echo ""
echo "======================================"
echo "✅ 모니터링 완료: $TIMESTAMP"
echo "======================================"
# 모니터링 완료 로그
echo "[$TIMESTAMP] 🔍 컨테이너 모니터링 완료 - 상태: $OVERALL_STATUS" | tee -a $LOG_FILE
exit 0
EOF
// 권한 설정
chmod +x monitor.sh
monitor.sh 스크립트를 실행하면, 컨테이너, 각종 API 등의 상태를 확인 후 보여준다.
완료되면 모니터링 로그를 로그 파일에 저장한다.
2.5. docker-compose.yaml 수정
cat > docker-compose.yaml << 'EOF'
version: '3.8'
services:
app:
build: .
container_name: sns-dev-app
restart: unless-stopped
ports:
- "3000:3000"
- "8080:8080"
environment:
- NODE_ENV=dev
env_file:
- src/configs/env/.dev.env
volumes:
- ./src:/app/src
- ./logs:/app/logs
- ./uploads:/app/uploads
- pnpm-store:/root/.pnpm-store
- /app/node_modules
dns:
- 168.126.63.1
- 8.8.8.8
- 8.8.4.4
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- dev-network
healthcheck:
test: ["CMD", "sh", "-c", "wget --quiet --tries=1 --spider http://localhost:3000/health && wget --quiet --tries=1 --spider http://localhost:8080"]
interval: 30s
timeout: 15s
retries: 3
start_period: 90s
postgres:
image: postgres:15-alpine
container_name: sns-dev-postgres
restart: always
ports:
- "5433:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
volumes:
- ./postgres-data:/var/lib/postgresql/data
networks:
- dev-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
redis:
image: redis:7.4-alpine
container_name: sns-dev-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- dev-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3
# 모니터링 서비스 (Git 설정 마운트 제거)
monitor:
build:
context: .
dockerfile: Dockerfile.monitor
container_name: sns-dev-monitor
restart: unless-stopped
volumes:
# Docker 소켓 마운트 (호스트 Docker 접근용)
- /var/run/docker.sock:/var/run/docker.sock:ro
# 프로젝트 디렉토리 마운트 (읽기 전용)
- .:/project:ro
# 모니터링 로그 저장용
- ./logs:/monitor/logs
# Git 설정 마운트 제거 (컨테이너 내부에서 직접 설정)
environment:
# 프로젝트 경로
- PROJECT_PATH=/project
# DDNS 호스트
- DDNS_HOST=${DDNS_HOST:-xxx.synology.me}
# 모니터링 설정
- MONITOR_INTERVAL=60
- LOG_RETENTION_DAYS=7
- HEALTH_CHECK_TIMEOUT=10
# Git 설정 (환경변수로 전달)
- GIT_SAFE_DIRECTORY=/project
- GIT_USER_NAME=${GIT_USER_NAME:-CI/CD Bot}
- GIT_USER_EMAIL=${GIT_USER_EMAIL:-email@example.com}
networks:
- dev-network
depends_on:
app:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "sh", "-c", "test -f /monitor/logs/monitor.log && test -x /monitor/monitor.sh"]
interval: 60s
timeout: 10s
retries: 3
start_period: 120s
# 보안 설정
security_opt:
- no-new-privileges:true
# 메모리 제한 (GitHub Actions 호환)
mem_limit: 256m
memswap_limit: 256m
volumes:
pnpm-store:
driver: local
redis-data:
driver: local
networks:
dev-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
driver: default
labels:
- "project=nestjs-sns"
- "environment=development"
- "ddns=${DDNS_HOST:-xxx.synology.me}"
EOF
기존의 docker-compose.yaml을 수정한다.
기존 스크립트에서 monitor 서비스 부분만 추가되었다.
2.6. 결과 확인
2.6.1. 로그 확인 방법들
방법1: NAS에서 직접 확인하기
# NAS에 SSH 접속 후
cd /volume1/docker/NestJS_SNS
# 모니터링 로그 파일 확인
sudo ls -la logs/
# 실시간 로그 확인
sudo tail -f logs/monitor.log
# 마지막 20줄 확인
sudo tail -20 logs/monitor.log
# 특정 시간대 로그 검색
sudo grep "2025-06-30 15:" logs/monitor.log
만약 실시간 로그 확인을 한다면 위 스크린샷처럼 현재 monitor 컨테이너의 로그를 보여준다.
방법2: Docker 명령어로 확인
# 컨테이너 내부 로그 파일 확인
sudo docker exec sns-dev-monitor ls -la /monitor/logs/
# 컨테이너 내부에서 로그 내용 확인
sudo docker exec sns-dev-monitor cat /monitor/logs/monitor.log
# 컨테이너 내부에서 실시간 로그 확인
sudo docker exec sns-dev-monitor tail -f /monitor/logs/monitor.log
마찬가지로 Docker 명령어로도 동일하게 볼 수 있다.
방법3: Docker Compose 로그 확인
# 모니터링 컨테이너의 실시간 로그 (stdout)
sudo docker-compose logs -f monitor
# 모니터링 컨테이너의 최근 로그
sudo docker-compose logs --tail=50 monitor
# 컨테이너 내부 로그 파일 짧게 확인
sudo docker-compose exec monitor tail -f /monitor/logs/monitor.log
결국 어떤 명령어로 볼 것인가는 취향차이라고 볼 수 있겠다.
3. 로그정리 스크립트 작성
monitor.sh 에서 정의된 log-rotate.sh는 UTC 시간 새벽3시 (한국시간 정오 12:00) 에 실행된다.
log-rotate.sh가 실행되면 기존에 쌓여있던 로그들을 정리하거나 삭제한다.
3.1. 프로젝트 경로 이동
cd /volume1/docker/NestJS_SNS
3.2. 로그정리 스크립트 작성
cat > log-rotate.sh << 'EOF'
#!/bin/bash
# 환경변수에서 설정 가져오기 (monitor.sh와 동일)
PROJECT_PATH="${PROJECT_PATH:-/project}"
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-7}"
LOGS_DIR="/monitor/logs"
PROJECT_LOGS_DIR="$PROJECT_PATH/logs"
BACKUP_DIR="$LOGS_DIR/archive"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
LOG_FILE="$LOGS_DIR/log-rotate.log"
# 디렉토리 생성
mkdir -p "$BACKUP_DIR"
mkdir -p "$PROJECT_LOGS_DIR"
# 함수: 로그 메시지 출력
log_message() {
local message="$1"
echo "[$TIMESTAMP] $message" | tee -a $LOG_FILE
}
# 함수: 파일 크기를 사람이 읽기 쉬운 형태로 변환
human_size() {
local size_bytes="$1"
if [ "$size_bytes" -gt 1073741824 ]; then
echo "$(( size_bytes / 1073741824 ))GB"
elif [ "$size_bytes" -gt 1048576 ]; then
echo "$(( size_bytes / 1048576 ))MB"
elif [ "$size_bytes" -gt 1024 ]; then
echo "$(( size_bytes / 1024 ))KB"
else
echo "${size_bytes}B"
fi
}
echo "=====================================" | tee -a $LOG_FILE
log_message "🧹 로그 로테이션 시작 (보관기간: ${LOG_RETENTION_DAYS}일)"
echo "=====================================" | tee -a $LOG_FILE
# 현재 로그 상태 확인
log_message "📊 현재 로그 상태:"
echo " - 모니터링 로그 디렉토리: $LOGS_DIR" | tee -a $LOG_FILE
echo " - 프로젝트 로그 디렉토리: $PROJECT_LOGS_DIR" | tee -a $LOG_FILE
# 모니터링 로그 파일 수 계산
MONITOR_LOG_COUNT=$(find "$LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
PROJECT_LOG_COUNT=$(find "$PROJECT_LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
echo " - 모니터링 로그 파일: ${MONITOR_LOG_COUNT}개" | tee -a $LOG_FILE
echo " - 프로젝트 로그 파일: ${PROJECT_LOG_COUNT}개" | tee -a $LOG_FILE
if [ -d "$LOGS_DIR" ]; then
TOTAL_SIZE=$(du -sb "$LOGS_DIR" 2>/dev/null | awk '{print $1}' || echo "0")
echo " - 총 로그 크기: $(human_size $TOTAL_SIZE)" | tee -a $LOG_FILE
fi
# 1. 모니터링 로그 로테이션 (monitor.log가 1000줄 초과시)
log_message "🔄 모니터링 로그 로테이션 확인"
MONITOR_LOG_MAIN="$LOGS_DIR/monitor.log"
if [ -f "$MONITOR_LOG_MAIN" ]; then
LINES=$(wc -l < "$MONITOR_LOG_MAIN" 2>/dev/null || echo "0")
if [ "$LINES" -gt 1000 ]; then
ROTATED_NAME="monitor.log.$(date +%Y%m%d-%H%M%S)"
mv "$MONITOR_LOG_MAIN" "$LOGS_DIR/$ROTATED_NAME"
touch "$MONITOR_LOG_MAIN"
log_message " ✅ 로테이션: monitor.log → $ROTATED_NAME ($LINES줄)"
else
log_message " ℹ️ monitor.log 로테이션 불필요 ($LINES줄)"
fi
else
log_message " ℹ️ monitor.log 파일 없음"
fi
# 2. LOG_RETENTION_DAYS 이상된 로그 파일 백업
log_message "📦 ${LOG_RETENTION_DAYS}일 이상된 로그 파일 백업"
# 모니터링 로그 백업
OLD_MONITOR_LOGS=$(find "$LOGS_DIR" -maxdepth 1 -name "*.log.*" -mtime +$LOG_RETENTION_DAYS 2>/dev/null)
BACKUP_COUNT=0
if [ -n "$OLD_MONITOR_LOGS" ]; then
echo "$OLD_MONITOR_LOGS" | while read logfile; do
if [ -f "$logfile" ]; then
filename=$(basename "$logfile")
mv "$logfile" "$BACKUP_DIR/$filename"
log_message " ✅ 백업: $filename"
BACKUP_COUNT=$((BACKUP_COUNT + 1))
fi
done
else
log_message " ℹ️ 백업할 오래된 모니터링 로그가 없습니다."
fi
# 프로젝트 로그 백업
if [ -d "$PROJECT_LOGS_DIR" ]; then
OLD_PROJECT_LOGS=$(find "$PROJECT_LOGS_DIR" -name "*.log.*" -mtime +$LOG_RETENTION_DAYS 2>/dev/null)
if [ -n "$OLD_PROJECT_LOGS" ]; then
echo "$OLD_PROJECT_LOGS" | while read logfile; do
if [ -f "$logfile" ]; then
filename=$(basename "$logfile")
backup_name="project_${filename}"
mv "$logfile" "$BACKUP_DIR/$backup_name"
log_message " ✅ 백업: $filename → $backup_name"
fi
done
else
log_message " ℹ️ 백업할 오래된 프로젝트 로그가 없습니다."
fi
fi
# 3. 30일 이상된 백업 파일 압축
log_message "🗜️ 30일 이상된 백업 파일 압축"
OLD_BACKUPS=$(find "$BACKUP_DIR" -name "*.log*" -not -name "*.gz" -mtime +30 2>/dev/null)
if [ -n "$OLD_BACKUPS" ]; then
echo "$OLD_BACKUPS" | while read backupfile; do
if [ -f "$backupfile" ]; then
gzip "$backupfile"
log_message " ✅ 압축: $(basename "$backupfile") → $(basename "$backupfile").gz"
fi
done
else
log_message " ℹ️ 압축할 백업 파일이 없습니다."
fi
# 4. 90일 이상된 압축 파일 삭제
log_message "🗑️ 90일 이상된 압축 파일 삭제"
VERY_OLD_BACKUPS=$(find "$BACKUP_DIR" -name "*.gz" -mtime +90 2>/dev/null)
if [ -n "$VERY_OLD_BACKUPS" ]; then
echo "$VERY_OLD_BACKUPS" | while read oldfile; do
if [ -f "$oldfile" ]; then
rm "$oldfile"
log_message " ✅ 삭제: $(basename "$oldfile")"
fi
done
else
log_message " ℹ️ 삭제할 오래된 압축 파일이 없습니다."
fi
# 5. Docker 로그 정리 (컨테이너 내부에서 실행시)
log_message "🐳 Docker 로그 정리"
if command -v docker >/dev/null 2>&1; then
# 중지된 컨테이너 정리 (SNS 관련 제외)
STOPPED_CONTAINERS=$(docker ps -a -q -f status=exited --filter "name!=sns-dev" 2>/dev/null)
if [ -n "$STOPPED_CONTAINERS" ]; then
docker rm $STOPPED_CONTAINERS >/dev/null 2>&1
log_message " ✅ 중지된 컨테이너 정리 완료"
else
log_message " ℹ️ 정리할 중지된 컨테이너가 없습니다."
fi
# 사용하지 않는 이미지 정리 (dangling 이미지만)
PRUNED=$(docker image prune -f 2>/dev/null | grep "Total reclaimed space" || echo "0B")
log_message " ✅ 사용하지 않는 이미지 정리 완료: $PRUNED"
# 컨테이너 로그 크기 확인
SNS_CONTAINERS=$(docker ps --format "{{.Names}}" | grep "sns-dev" 2>/dev/null)
if [ -n "$SNS_CONTAINERS" ]; then
echo "$SNS_CONTAINERS" | while read container; do
LOG_SIZE=$(docker logs --details "$container" 2>/dev/null | wc -c)
log_message " 📊 $container 로그 크기: $(human_size $LOG_SIZE)"
done
fi
else
log_message " ⚠️ Docker 명령어 접근 불가 (호스트에서 실행 시 정상)"
fi
# 6. 로테이션 후 상태 요약
log_message "📊 로테이션 후 상태:"
# 파일 개수 재계산
FINAL_MONITOR_LOGS=$(find "$LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
FINAL_PROJECT_LOGS=$(find "$PROJECT_LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
BACKUP_FILES=$(find "$BACKUP_DIR" -name "*.log*" -not -name "*.gz" 2>/dev/null | wc -l)
COMPRESSED_FILES=$(find "$BACKUP_DIR" -name "*.gz" 2>/dev/null | wc -l)
echo " - 활성 모니터링 로그: ${FINAL_MONITOR_LOGS}개" | tee -a $LOG_FILE
echo " - 활성 프로젝트 로그: ${FINAL_PROJECT_LOGS}개" | tee -a $LOG_FILE
echo " - 백업 파일: ${BACKUP_FILES}개" | tee -a $LOG_FILE
echo " - 압축 파일: ${COMPRESSED_FILES}개" | tee -a $LOG_FILE
if [ -d "$LOGS_DIR" ]; then
FINAL_SIZE=$(du -sb "$LOGS_DIR" 2>/dev/null | awk '{print $1}' || echo "0")
echo " - 총 크기: $(human_size $FINAL_SIZE)" | tee -a $LOG_FILE
fi
# 7. 디스크 공간 확인
log_message "💾 디스크 공간 상태:"
DISK_USAGE=$(df -h /monitor 2>/dev/null | tail -1 | awk '{print $5 " (" $4 " 사용가능)"}' || echo "확인불가")
echo " - 디스크 사용률: $DISK_USAGE" | tee -a $LOG_FILE
echo "=====================================" | tee -a $LOG_FILE
log_message "🎉 로그 로테이션 완료"
echo "=====================================" | tee -a $LOG_FILE
# 실행 결과 반환 (성공: 0, 실패: 1)
exit 0
EOF
3.3. 권한 설정
chmod +x log-rotate.sh
3.4. 수동 실행 가이드
# 1. 일반적인 수동 실행
docker exec sns-dev-monitor /monitor/log-rotate.sh
# 2. 로그가 너무 많이 쌓였을 때 (3일 이상 된 것만 정리)
docker exec -e LOG_RETENTION_DAYS=3 sns-dev-monitor /monitor/log-rotate.sh
# 3. 실행 과정을 실시간으로 확인
docker exec -it sns-dev-monitor /monitor/log-rotate.sh
# 4. 실행 후 결과 확인
docker exec sns-dev-monitor tail -20 /monitor/logs/log-rotate.log
4. 문제해결 가이드
# API 응답 체크
curl -v http://호스트:3000/health
# 컨테이너 상태 확인
docker-compose ps
docker-compose logs app
# 롤백 실행
./rollback.sh
# 완전 재시작
docker-compose down
docker-compose up -d --build
'NestJS' 카테고리의 다른 글
Github Actions를 사용한 CI/CD - 백업 시스템 구축 (3) | 2025.07.04 |
---|---|
Github Actions를 사용한 CI/CD - Slack 알림 (0) | 2025.07.03 |
Github Actions를 사용한 CI/CD 구축 (0) | 2025.06.30 |
시놀로지 나스 NestJS 개발서버 구축하기 - Docker Container (0) | 2025.06.19 |
[NestJS] winston 로그 구현하기 (1) | 2025.06.05 |