각종 환경설정이나 DB 데이터 등은 주기적으로 백업이 필요하다.
이전에 구현했던 monitor.sh처럼 도커 컨테이너에서 동작되며 주기적으로 백업을 진행하고 정리를 하는 시스템을 구축해보고자 한다.
그리고 마지막으로 수동 테스트를 진행하여 백업이 잘 진행되는지도 확인해볼 것이다.
0. 백업 시스템 개요
0.1. 백업 시스템 동작 방식
1. Github Actions 실행(deploy.yml)
2. deploy.sh 실행
3. docker-compose up
4. Dockerfile.backup 실행
5. backup-daemon.sh 실행
6. 스케줄 대기
7. backup.sh 자동 실행
0.2. 백업 시스템 수행 시간
시간 | 요일 | 백업 타입 | 저장 위치 |
매일 새벽 2시 (KST) | 월-토 | daily | /backup/data/daily/ |
일요일 새벽 2시 (KST) | 일요일 | weekly | /backup/data/weekly/ |
매월 1일 새벽 2시 (KST) | 1일 | monthly | /backup/data/monthly/ |
일요일 새벽 3시 (KST) | 일요일 | cleanup | 오래된 백업 삭제 |
1. 백업 시스템 구조
/volume1/docker/NestJS_SNS/
├── backup/
│ ├── backup.sh # 메인 백업 스크립트 (Docker용으로 수정)
│ ├── backup-daemon.sh # 🆕 Docker 데몬 스크립트
│ ├── restore.sh # 복원 스크립트 (Docker용으로 수정)
│ ├── backup-config.conf # 백업 설정 파일 (Docker용으로 수정)
│ ├── cleanup.sh # 정리 스크립트 (Docker용으로 수정)
│ ├── data/ # 🆕 백업 데이터 (Docker 볼륨)
│ │ ├── daily/
│ │ ├── weekly/
│ │ ├── monthly/
│ │ ├── emergency/
│ │ └── database/
│ └── logs/ # 로그 파일 (Docker 볼륨)
├── Dockerfile.backup # 🆕 백업 전용 Dockerfile
└── docker-compose.yml # backup 서비스 추가
1.1. backup 폴더 생성
mkdir -p backup
1.2. backup 폴더 권한 설정
# 백업 폴더 전체 권한 설정 (한 번에!)
chmod -R 755 backup/
1.3. deploy.sh 스크립트 수정
cat > deploy.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
# 비밀번호를 첫 번째 인수로 받기
SUDO_PASSWORD="$1"
# DDNS 호스트를 두 번째 인수로 받기 (기본값: xxx.synology.me)
DDNS_HOST="${2:-xxx.synology.me}"
# 설정 변수
PROJECT_PATH="/volume1/docker/NestJS_SNS"
LOG_FILE="$PROJECT_PATH/logs/deploy.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
# 역방향 프록시 DDNS 설정
API_URL="https://$DDNS_HOST"
DOCS_URL="https://$DDNS_HOST:444"
# 보존할 베이스 이미지들
PRESERVE_IMAGES="postgres:15-alpine redis:7-alpine redis:7.4-alpine node:20-alpine alpine:latest"
# 보존할 컨테이너들 (데이터베이스 컨테이너들)
PRESERVE_CONTAINERS="sns-dev-postgres sns-dev-redis"
# 정리할 애플리케이션 컨테이너들 (백업 포함)
CLEANUP_CONTAINERS="sns-dev-app sns-dev-monitor sns-dev-backup nestjs_sns_app 2_cf_sns-app"
# 🆕 로그 함수 정의 (먼저 정의)
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $level $message" | tee -a "$LOG_FILE"
}
# 로그 디렉토리 생성
mkdir -p "$PROJECT_PATH/logs"
# 프로젝트 디렉토리로 이동 (디렉토리 생성보다 먼저)
cd $PROJECT_PATH || {
echo "❌ 프로젝트 디렉토리 접근 실패: $PROJECT_PATH"
exit 1
}
# 🆕 백업 시스템 디렉토리 준비 (상대경로로 변경)
log "INFO" "📁 백업 시스템 디렉토리 준비 중..."
# 현재 작업 디렉토리 확인
log "INFO" "현재 작업 디렉토리: $(pwd)"
# 백업 디렉토리 강제 생성 (상대경로 사용)
log "INFO" "백업 디렉토리 강제 생성 중..."
mkdir -p backup/data/daily
mkdir -p backup/data/weekly
mkdir -p backup/data/monthly
mkdir -p backup/data/emergency
mkdir -p backup/data/database
mkdir -p backup/logs
# 권한 설정
chmod -R 755 backup/
chmod +x backup/*.sh 2>/dev/null || true
# 생성 확인
if [[ -d "backup/logs" ]]; then
log "INFO" "✅ 백업 디렉토리 생성 확인됨"
ls -la backup/ | tee -a $LOG_FILE
else
log "ERROR" "❌ 백업 디렉토리 생성 실패"
exit 1
fi
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 || {
log "ERROR" "❌ 프로젝트 디렉토리 접근 실패: $PROJECT_PATH"
exit 1
}
log "INFO" "✅ 모든 디렉토리 준비 완료"
# Docker 심볼릭 링크 생성 (필요시)
log "INFO" "🔧 Docker 심볼릭 링크 확인"
if [ ! -L "/usr/bin/docker" ]; then
echo "$SUDO_PASSWORD" | sudo -S ln -sf /usr/local/bin/docker /usr/bin/docker
log "INFO" "✅ Docker 심볼릭 링크 생성"
fi
if [ ! -L "/usr/bin/docker-compose" ]; then
echo "$SUDO_PASSWORD" | sudo -S ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
log "INFO" "✅ Docker Compose 심볼릭 링크 생성"
fi
# Docker 명령어 경로 확인
log "INFO" "🔧 Docker 명령어 경로 확인"
log "INFO" "Docker 경로: $(which docker)"
log "INFO" "Docker Compose 경로: $(which docker-compose)"
# 최신 코드 가져오기
log "INFO" "📥 최신 코드 가져오기 시작"
if git pull origin main 2>&1 | tee -a $LOG_FILE; then
log "INFO" "✅ Git pull 성공"
else
log "ERROR" "❌ Git pull 실패"
exit 1
fi
# 현재 실행 중인 컨테이너 확인
log "INFO" "🔍 현재 실행 중인 컨테이너 확인"
echo "$SUDO_PASSWORD" | sudo -S docker ps -a | tee -a $LOG_FILE
# ========== 🆕 완전한 정리 단계 ==========
log "INFO" "🧹 완전한 정리 시작 (DB 컨테이너 및 볼륨 보존)"
# 1. Docker Compose 서비스 완전 중지 및 제거 (볼륨은 보존)
log "INFO" "🛑 Docker Compose 서비스 완전 정리 (볼륨 보존)"
echo "$SUDO_PASSWORD" | sudo -S docker-compose down --remove-orphans 2>&1 | tee -a $LOG_FILE
# 2. 문제가 있는 컨테이너들 강제 제거
log "INFO" "🗑️ 애플리케이션 컨테이너 강제 제거"
for container in $CLEANUP_CONTAINERS; do
if echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then
log "INFO" "🛑 컨테이너 강제 제거: $container"
echo "$SUDO_PASSWORD" | sudo -S docker stop "$container" 2>/dev/null || true
echo "$SUDO_PASSWORD" | sudo -S docker rm -f "$container" 2>/dev/null || true
else
log "INFO" "✅ 컨테이너 없음: $container"
fi
done
# 3. 보존할 컨테이너 상태 확인
log "INFO" "🔍 보존할 컨테이너 상태 확인"
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)
log "INFO" "🛡️ 보존된 컨테이너: $container - $CONTAINER_STATUS"
else
log "WARN" "⚠️ 보존할 컨테이너 없음: $container (새로 생성됨)"
fi
done
# 4. 애플리케이션 이미지만 선택적 정리 (DB 이미지 보존)
log "INFO" "🖼️ 애플리케이션 이미지 선택적 정리"
log "INFO" "🛡️ 보존할 이미지: $PRESERVE_IMAGES"
# 애플리케이션 이미지만 찾기 (sns-dev 관련, 백업 포함)
APP_IMAGES=$(echo "$SUDO_PASSWORD" | sudo -S docker images --format "{{.Repository}}:{{.Tag}}" | grep -E "(sns-dev|nestjs_sns|2_cf_sns)" || true)
if [ -n "$APP_IMAGES" ]; then
echo "$APP_IMAGES" | while read image; do
if [ -n "$image" ]; then
# 보존할 이미지인지 확인
SHOULD_PRESERVE=false
for preserve_image in $PRESERVE_IMAGES; do
if echo "$image" | grep -q "$preserve_image"; then
SHOULD_PRESERVE=true
break
fi
done
if [ "$SHOULD_PRESERVE" = false ]; then
log "INFO" "🗑️ 애플리케이션 이미지 제거: $image"
echo "$SUDO_PASSWORD" | sudo -S docker rmi -f "$image" 2>/dev/null || true
else
log "INFO" "🛡️ 보존된 이미지: $image"
fi
fi
done
else
log "INFO" "✅ 제거할 애플리케이션 이미지가 없습니다."
fi
# 5. 댕글링 이미지 정리
log "INFO" "🧹 댕글링 이미지 정리"
echo "$SUDO_PASSWORD" | sudo -S docker image prune -f 2>&1 | tee -a $LOG_FILE
# 6. 정리 후 상태 확인
log "INFO" "📋 정리 후 상태 확인"
log "INFO" "=== 현재 컨테이너 상태 ==="
echo "$SUDO_PASSWORD" | sudo -S docker ps -a | tee -a $LOG_FILE
log "INFO" "=== 보존된 이미지 확인 ==="
for preserve_image in $PRESERVE_IMAGES; do
if echo "$SUDO_PASSWORD" | sudo -S docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^$preserve_image$"; then
log "INFO" "✅ 보존된 이미지: $preserve_image"
else
log "WARN" "⚠️ 이미지 없음: $preserve_image (다운로드 필요)"
fi
done
log "INFO" "✅ 완전한 정리 완료 - DB 컨테이너와 이미지 보존됨!"
# ========== 🆕 Docker 빌드 및 시작 단계 ==========
log "INFO" "🐳 Docker 컨테이너 빌드 및 시작"
log "INFO" "💡 빌드 과정을 실시간으로 모니터링합니다..."
# 모든 서비스 빌드 (app, monitor, backup)
log "INFO" "🔨 Docker 이미지 빌드 시작 (app + monitor + backup)"
echo "$SUDO_PASSWORD" | sudo -S docker-compose build --no-cache app monitor backup 2>&1 | tee -a $LOG_FILE
BUILD_EXIT_CODE=${PIPESTATUS[1]}
if [ $BUILD_EXIT_CODE -eq 0 ]; then
log "INFO" "✅ Docker 이미지 빌드 성공 (app + monitor + backup)"
# 🆕 새로운 컨테이너 시작 (강제 재생성)
log "INFO" "🚀 Docker 컨테이너 새로 시작 (강제 재생성)"
echo "$SUDO_PASSWORD" | sudo -S docker-compose up -d --force-recreate 2>&1 | tee -a $LOG_FILE
UP_EXIT_CODE=${PIPESTATUS[1]}
if [ $UP_EXIT_CODE -eq 0 ]; then
log "INFO" "✅ Docker 컨테이너 시작 성공"
else
log "ERROR" "❌ Docker 컨테이너 시작 실패 (Exit Code: $UP_EXIT_CODE)"
exit 1
fi
else
log "ERROR" "❌ Docker 이미지 빌드 실패 (Exit Code: $BUILD_EXIT_CODE)"
exit 1
fi
# 빌드 후 컨테이너 상태 즉시 확인
log "INFO" "📋 빌드 후 컨테이너 상태 확인"
echo "$SUDO_PASSWORD" | sudo -S docker-compose ps | tee -a $LOG_FILE
# 모든 컨테이너 상태 확인
log "INFO" "📋 전체 컨테이너 상태"
echo "$SUDO_PASSWORD" | sudo -S docker ps -a | tee -a $LOG_FILE
# 동적 서비스 시작 대기 (HTTPS로 변경)
log "INFO" "⏳ 서비스 시작 대기 중..."
for i in {1..12}; do # 최대 60초 대기 (5초씩 12번)
# HTTPS 헬스체크로 변경 (SSL 검증 무시)
if curl -k -f "$API_URL/health" > /dev/null 2>&1; then
log "INFO" "✅ 서비스 시작 완료 ($((i*5))초 소요)"
break
else
log "INFO" "⏳ 서비스 시작 대기 중... ($((i*5))초)"
# 5번째 시도 후부터 컨테이너 로그 확인
if [ $i -eq 5 ]; then
log "INFO" "🔍 애플리케이션 컨테이너 로그 확인 (최근 10줄)"
echo "$SUDO_PASSWORD" | sudo -S docker logs --tail=10 sns-dev-app 2>&1 | tee -a $LOG_FILE || log "WARN" "앱 컨테이너 로그를 가져올 수 없습니다."
fi
sleep 5
fi
if [ $i -eq 12 ]; then
log "WARN" "⚠️ 서비스 시작 시간이 오래 걸리고 있습니다."
# 최종 디버깅 정보
log "INFO" "🔍 최종 디버깅 정보"
log "INFO" "네트워크 상태:"
echo "$SUDO_PASSWORD" | sudo -S docker network ls | tee -a $LOG_FILE
log "INFO" "컨테이너 상세 상태:"
echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | tee -a $LOG_FILE
exit 1
fi
done
# 헬스체크 수행 (HTTPS로 변경)
log "INFO" "🔍 헬스체크 수행 (HTTPS 역방향 프록시)"
# NestJS API 헬스체크 (HTTPS)
log "INFO" "🔍 NestJS API 헬스체크 (HTTPS)"
if curl -k -f "$API_URL/health" > /dev/null 2>&1; then
log "INFO" "✅ NestJS API HTTPS 헬스체크 통과"
# API 응답 내용도 확인
API_RESPONSE=$(curl -k -s "$API_URL/health")
log "INFO" "🧪 API 응답: $API_RESPONSE"
else
log "ERROR" "❌ NestJS API HTTPS 헬스체크 실패"
# 대체 헬스체크: 내부 네트워크로 시도
log "INFO" "🔄 내부 네트워크 헬스체크 시도"
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
log "INFO" "✅ 내부 네트워크 헬스체크 통과 (역방향 프록시 설정 확인 필요)"
else
log "ERROR" "❌ 내부 네트워크 헬스체크도 실패"
exit 1
fi
fi
# Swagger UI 접근 확인
log "INFO" "🔍 Swagger UI 접근 확인"
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
log "INFO" "✅ Swagger UI 접근 가능 (HTTP $SWAGGER_STATUS)"
else
log "WARN" "⚠️ Swagger UI 접근 상태: HTTP $SWAGGER_STATUS"
fi
# GraphQL 접근 확인
log "INFO" "🔍 GraphQL 접근 확인"
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
log "INFO" "✅ GraphQL 엔드포인트 접근 가능 (HTTP $GRAPHQL_STATUS)"
else
log "WARN" "⚠️ GraphQL 엔드포인트 상태: HTTP $GRAPHQL_STATUS"
fi
# Compodoc 문서 접근 확인
log "INFO" "🔍 Compodoc 문서 접근 확인"
DOCS_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$DOCS_URL/" 2>/dev/null)
if [ "$DOCS_STATUS" = "200" ]; then
log "INFO" "✅ Compodoc 문서 접근 가능 (HTTP $DOCS_STATUS)"
else
log "WARN" "⚠️ Compodoc 문서 상태: HTTP $DOCS_STATUS"
fi
# 모니터링 컨테이너 상태 확인
log "INFO" "🔍 모니터링 컨테이너 상태 확인"
MONITOR_STATUS=$(echo "$SUDO_PASSWORD" | sudo -S docker ps --format "{{.Names}}\t{{.Status}}" | grep "sns-dev-monitor" || echo "없음")
if [ "$MONITOR_STATUS" != "없음" ]; then
log "INFO" "✅ 모니터링 컨테이너: $MONITOR_STATUS"
# 모니터링 로그 확인 (최근 5줄)
log "INFO" "📊 모니터링 컨테이너 로그 (최근 5줄):"
echo "$SUDO_PASSWORD" | sudo -S docker logs --tail=5 sns-dev-monitor 2>&1 | tee -a $LOG_FILE
else
log "ERROR" "❌ 모니터링 컨테이너가 실행되지 않았습니다."
fi
# 🆕 백업 컨테이너 상태 확인
log "INFO" "🔍 백업 컨테이너 상태 확인"
BACKUP_STATUS=$(echo "$SUDO_PASSWORD" | sudo -S docker ps --format "{{.Names}}\t{{.Status}}" | grep "sns-dev-backup" || echo "없음")
if [ "$BACKUP_STATUS" != "없음" ]; then
log "INFO" "✅ 백업 컨테이너: $BACKUP_STATUS"
# 백업 로그 확인 (최근 5줄)
log "INFO" "🗃️ 백업 컨테이너 로그 (최근 5줄):"
echo "$SUDO_PASSWORD" | sudo -S docker logs --tail=5 sns-dev-backup 2>&1 | tee -a $LOG_FILE
else
log "ERROR" "❌ 백업 컨테이너가 실행되지 않았습니다."
fi
log "INFO" "🌐 서비스 접근 정보 (역방향 프록시)"
log "INFO" " 🔒 HTTPS 접근 (권장):"
log "INFO" " 📡 NestJS API: $API_URL"
log "INFO" " 📋 Swagger UI: $API_URL/api"
log "INFO" " 🚀 GraphQL: $API_URL/graphql"
log "INFO" " 🔍 Health Check: $API_URL/health"
log "INFO" " 📚 Compodoc: $DOCS_URL"
log "INFO" " 📊 모니터링:"
log "INFO" " 🔍 모니터링 로그: docker exec sns-dev-monitor tail -f /monitor/logs/monitor.log"
log "INFO" " 📈 실시간 모니터링: docker exec sns-dev-monitor ./monitor.sh"
log "INFO" " 🗃️ 백업:"
log "INFO" " 🔍 백업 로그: docker exec sns-dev-backup tail -f /backup/logs/backup.log"
log "INFO" " 📋 백업 목록: docker exec sns-dev-backup /backup/scripts/restore.sh list"
log "INFO" " 🚨 긴급 백업: docker exec sns-dev-backup /backup/scripts/backup.sh emergency"
echo "=====================================" | tee -a $LOG_FILE
log "INFO" "🎉 배포 프로세스 완료"
echo "=====================================" | tee -a $LOG_FILE
# 명시적으로 성공 상태 반환
log "INFO" "📤 배포 성공 상태 반환 (Exit Code: 0)"
exit 0
EOF
deploy.sh 를 수정해야하는데, /backup 의 권한 설정, backup 컨테이너 빌드 등을 수정한다.
2. 도커 설정 파일 생성 (backup-config.conf)
cat > /volume1/docker/NestJS_SNS/backup/backup-config.conf << 'EOF'
# ==============================================
# 🔧 백업 시스템 설정 파일 (Docker용)
# ==============================================
# 📂 백업 대상 경로 (Docker 볼륨 마운트 기준)
PROJECT_ROOT="/app"
SOURCE_CODE_DIR="${PROJECT_ROOT}"
DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml"
ENV_FILE="${PROJECT_ROOT}/src/configs/env/.dev.env"
# 📁 백업 저장 경로 (컨테이너 내부)
BACKUP_BASE_DIR="/backup/data"
DAILY_BACKUP_DIR="${BACKUP_BASE_DIR}/daily"
WEEKLY_BACKUP_DIR="${BACKUP_BASE_DIR}/weekly"
MONTHLY_BACKUP_DIR="${BACKUP_BASE_DIR}/monthly"
EMERGENCY_BACKUP_DIR="${BACKUP_BASE_DIR}/emergency"
# 🗂️ 데이터베이스 설정 (실제 docker-compose.yml과 일치)
DB_CONTAINER_NAME="sns-dev-postgres"
DB_HOST="sns-dev-postgres"
DB_PORT="5432"
DB_USER="postgres"
DB_PASSWORD="postgres"
DB_NAME="postgres"
DB_BACKUP_DIR="${BACKUP_BASE_DIR}/database"
# 📦 Docker 컨테이너 설정 (실제 컨테이너명과 일치)
APP_CONTAINER_NAME="sns-dev-app"
REDIS_CONTAINER_NAME="sns-dev-redis"
MONITOR_CONTAINER_NAME="sns-dev-monitor"
BACKUP_CONTAINER_NAME="sns-dev-backup"
# ⏰ 백업 보존 기간 (일)
DAILY_RETENTION_DAYS=7
WEEKLY_RETENTION_DAYS=30
MONTHLY_RETENTION_DAYS=365
EMERGENCY_RETENTION_DAYS=90
# 📊 압축 설정
COMPRESSION_LEVEL=6
EXCLUDE_PATTERNS="node_modules|.git|dist|coverage|*.log|.DS_Store|backup/data|postgres-data|redis-data|n8n-data"
# 🔔 알림 설정
SLACK_WEBHOOK_URL=""
ENABLE_SLACK_NOTIFICATIONS=false
# 📈 로그 설정
LOG_DIR="/backup/logs"
LOG_FILE="${LOG_DIR}/backup.log"
MAX_LOG_SIZE="100M"
LOG_RETENTION_DAYS=30
# 🆕 백업 실행 환경 설정
BACKUP_ROOT="/backup/data"
SCRIPT_DIR="/backup/scripts"
EOF
Docker용 백업 시스템 설정 파일을 만든다.
Slack 알림이 필요하면 이전에 만들었던 웹훅 URL을 넣어주면 된다.
3. 백업 전용 Dockerfile 생성 (Dockerfile.backup)
cat > /volume1/docker/NestJS_SNS/Dockerfile.backup << 'EOF'
FROM alpine:3.18
# 필요한 패키지 설치
RUN apk add --no-cache \
bash \
curl \
gzip \
tar \
postgresql-client \
docker-cli \
openssh-client \
findutils \
coreutils \
tzdata
# 타임존 설정
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 백업 디렉토리 생성
RUN mkdir -p /backup/scripts /backup/data /backup/logs
# 작업 디렉토리 설정
WORKDIR /backup
# 백업 스크립트들 복사
COPY backup/ /backup/scripts/
RUN chmod +x /backup/scripts/*.sh
# 헬스체크 추가
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD [ -f /backup/logs/backup.log ] || exit 1
# 백업 데몬 실행
CMD ["/backup/scripts/backup-daemon.sh"]
EOF
docker-compose.yaml이 실행되면서 해당 Dockerfile이 실행된다.
Dockerfile은 backup-daemon.sh 파일을 실행한다.
4. docker-compose.yaml에 Dockerfile.backup 추가
# 기존 docker-compose.yml에 백업 서비스 추가
cat >> /volume1/docker/NestJS_SNS/docker-compose.yml << 'EOF'
# 🗃️ 백업 서비스
backup:
build:
context: .
dockerfile: Dockerfile.backup
container_name: sns-dev-backup
restart: unless-stopped
volumes:
# 프로젝트 소스 (읽기 전용)
- .:/app:ro
# Docker 소켓 (컨테이너 관리용)
- /var/run/docker.sock:/var/run/docker.sock:ro
# 백업 데이터 저장소
- ./backup/data:/backup/data
# 백업 로그
- ./backup/logs:/backup/logs
# 백업 설정 (읽기 전용)
- ./backup:/backup/scripts:ro
networks:
- sns-network
depends_on:
- db
- app
environment:
- TZ=Asia/Seoul
labels:
- "com.docker.compose.service=backup"
healthcheck:
test: ["CMD", "test", "-f", "/backup/logs/backup.log"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
EOF
기존 docker-compose.yaml 에서 추가로 backup에 해당하는 컨테이너를 구성한다.
전체 코드 보기
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
# 🗃️ 백업 서비스
backup:
build:
context: .
dockerfile: Dockerfile.backup
container_name: sns-dev-backup
restart: unless-stopped
volumes:
# 프로젝트 소스 (읽기 전용)
- .:/app:ro
# Docker 소켓 (컨테이너 관리용)
- /var/run/docker.sock:/var/run/docker.sock:ro
# 백업 데이터 저장소
- ./backup/data:/backup/data
# 백업 로그
- ./backup/logs:/backup/logs
# 백업 설정 (읽기 전용)
- ./backup:/backup/scripts:ro
networks:
- dev-network
depends_on:
postgres:
condition: service_healthy
app:
condition: service_healthy
environment:
- TZ=Asia/Seoul
# 백업 설정 환경변수
- BACKUP_SCHEDULE_ENABLED=true
- BACKUP_RETENTION_DAYS=7
- SLACK_NOTIFICATIONS_ENABLED=false
# Git 정보 (백업 메타데이터용)
- GIT_SAFE_DIRECTORY=/app
- GIT_USER_NAME=${GIT_USER_NAME:-Backup Bot}
- GIT_USER_EMAIL=${GIT_USER_EMAIL:-backup@example.com}
labels:
- "com.docker.compose.service=backup"
- "project=nestjs-sns"
- "component=backup"
healthcheck:
test: ["CMD", "test", "-f", "/backup/logs/backup.log"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# 보안 설정
security_opt:
- no-new-privileges:true
# 메모리 제한
mem_limit: 512m
memswap_limit: 512m
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
5. 백업 데몬 스크립트 생성 (backup-daemon.sh)
cat > /volume1/docker/NestJS_SNS/backup/backup-daemon.sh << 'EOF'
#!/bin/bash
# ==============================================
# 🔄 백업 데몬 (수정된 버전)
# ==============================================
set -euo pipefail
# 설정 로드
source /backup/scripts/backup-config.conf
# 🆕 한국 시간대 설정
export TZ=Asia/Seoul
# 로그 함수
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M:%S KST')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# 신호 처리
cleanup() {
log "INFO" "백업 데몬 종료 중..."
exit 0
}
trap cleanup SIGTERM SIGINT
# 초기화
initialize() {
log "INFO" "=== 백업 데몬 시작 (한국 시간 기준) ==="
log "INFO" "컨테이너 ID: $(hostname)"
log "INFO" "시작 시간: $(TZ=Asia/Seoul date)"
log "INFO" "시간대: Asia/Seoul (KST)"
# 필요한 디렉토리 생성
mkdir -p "$LOG_DIR"
mkdir -p "$DAILY_BACKUP_DIR"
mkdir -p "$WEEKLY_BACKUP_DIR"
mkdir -p "$MONTHLY_BACKUP_DIR"
mkdir -p "$EMERGENCY_BACKUP_DIR"
mkdir -p "$DB_BACKUP_DIR"
log "INFO" "백업 데몬 초기화 완료"
}
# 다음 백업 시간 계산 (한국 시간 기준)
calculate_next_backup() {
local current_hour=$(TZ=Asia/Seoul date +%H)
local current_minute=$(TZ=Asia/Seoul date +%M)
# 한국 시간 새벽 2시로 설정
local target_hour=2
local target_minute=0
if [[ $current_hour -lt $target_hour ]] || [[ $current_hour -eq $target_hour && $current_minute -lt $target_minute ]]; then
# 오늘 새벽 2시 (KST)
echo $(TZ=Asia/Seoul date -d "today ${target_hour}:${target_minute}" +%s)
else
# 내일 새벽 2시 (KST)
echo $(TZ=Asia/Seoul date -d "tomorrow ${target_hour}:${target_minute}" +%s)
fi
}
# 다음 정리 시간 계산 (한국 시간 일요일 새벽 3시)
calculate_next_cleanup() {
local current_day=$(TZ=Asia/Seoul date +%u) # 1=월요일, 7=일요일
local current_hour=$(TZ=Asia/Seoul date +%H)
local current_minute=$(TZ=Asia/Seoul date +%M)
local target_hour=3
local target_minute=0
if [[ $current_day -eq 7 ]] && [[ $current_hour -lt $target_hour ]]; then
# 이번 일요일 새벽 3시 (KST)
echo $(TZ=Asia/Seoul date -d "today ${target_hour}:${target_minute}" +%s)
else
# 다음 일요일 새벽 3시 (KST)
local days_until_sunday=$((7 - current_day))
if [[ $current_day -eq 7 ]]; then
days_until_sunday=7
fi
echo $(TZ=Asia/Seoul date -d "+${days_until_sunday} days ${target_hour}:${target_minute}" +%s)
fi
}
# 백업 타입 예측 함수
predict_backup_type() {
local target_date="$1" # timestamp
local day_of_week=$(TZ=Asia/Seoul date -d "@$target_date" +%u)
local day_of_month=$(TZ=Asia/Seoul date -d "@$target_date" +%d)
if [[ "$day_of_month" == "01" ]]; then
echo "monthly"
elif [[ "$day_of_week" == "7" ]]; then
echo "weekly"
else
echo "daily"
fi
}
# 🆕 정확한 백업 파일 카운팅
count_backup_files() {
# 데이터베이스 백업 (모든 타입 포함)
local db_count=$(find "$DB_BACKUP_DIR" -name "*.sql.gz" 2>/dev/null | wc -l)
# 각 타입별 백업 파일 카운팅
local daily_source=$(find "$DAILY_BACKUP_DIR" -name "source_*.tar.gz" 2>/dev/null | wc -l)
local daily_docker=$(find "$DAILY_BACKUP_DIR" -name "docker_*.tar.gz" 2>/dev/null | wc -l)
local daily_total=$((daily_source + daily_docker))
local weekly_source=$(find "$WEEKLY_BACKUP_DIR" -name "source_*.tar.gz" 2>/dev/null | wc -l)
local weekly_docker=$(find "$WEEKLY_BACKUP_DIR" -name "docker_*.tar.gz" 2>/dev/null | wc -l)
local weekly_total=$((weekly_source + weekly_docker))
local monthly_source=$(find "$MONTHLY_BACKUP_DIR" -name "source_*.tar.gz" 2>/dev/null | wc -l)
local monthly_docker=$(find "$MONTHLY_BACKUP_DIR" -name "docker_*.tar.gz" 2>/dev/null | wc -l)
local monthly_total=$((monthly_source + monthly_docker))
local emergency_source=$(find "$EMERGENCY_BACKUP_DIR" -name "source_*.tar.gz" 2>/dev/null | wc -l)
local emergency_docker=$(find "$EMERGENCY_BACKUP_DIR" -name "docker_*.tar.gz" 2>/dev/null | wc -l)
local emergency_total=$((emergency_source + emergency_docker))
# 결과 출력 (공백으로 구분)
echo "$db_count $daily_total $weekly_total $monthly_total $emergency_total"
}
# 백업 실행
run_backup() {
local backup_type=$(predict_backup_type $(date +%s))
log "INFO" "스케줄된 백업 실행 중... (타입: $backup_type)"
# 백업 타입에 따른 추가 로그
case "$backup_type" in
"monthly")
log "INFO" "🗓️ 월간 백업 실행 (매월 1일)"
;;
"weekly")
log "INFO" "📅 주간 백업 실행 (일요일)"
;;
"daily")
log "INFO" "📆 일간 백업 실행"
;;
esac
if /backup/scripts/backup.sh auto; then
log "INFO" "✅ $backup_type 백업 성공적으로 완료"
else
log "ERROR" "❌ $backup_type 백업 실행 중 오류 발생"
fi
}
# 정리 실행 (보존 정책 적용)
run_cleanup() {
log "INFO" "스케줄된 정리 작업 실행 중..."
log "INFO" "보존 정책: 일간(${DAILY_RETENTION_DAYS}일), 주간(${WEEKLY_RETENTION_DAYS}일), 월간(${MONTHLY_RETENTION_DAYS}일)"
# 자동 정리 (오래된 백업 삭제)
if /backup/scripts/cleanup.sh old ${DAILY_RETENTION_DAYS}; then
log "INFO" "✅ 정리 작업 성공적으로 완료"
else
log "ERROR" "❌ 정리 작업 실행 중 오류 발생"
fi
}
# 🆕 개선된 상태 리포트
status_report() {
log "INFO" "=== 백업 데몬 상태 ==="
log "INFO" "현재 시간: $(TZ=Asia/Seoul date)"
# 🆕 BusyBox 호환 가동시간 표시
local uptime_info=$(uptime | awk '{print $3, $4}' | sed 's/,//')
log "INFO" "가동 시간: $uptime_info"
log "INFO" "다음 백업: $(TZ=Asia/Seoul date -d @$next_backup_time) ($(predict_backup_type $next_backup_time))"
log "INFO" "다음 정리: $(TZ=Asia/Seoul date -d @$next_cleanup_time)"
# 🆕 정확한 백업 통계
local backup_counts=($(count_backup_files))
local db_count=${backup_counts[0]}
local daily_count=${backup_counts[1]}
local weekly_count=${backup_counts[2]}
local monthly_count=${backup_counts[3]}
local emergency_count=${backup_counts[4]}
log "INFO" "📊 백업 현황:"
log "INFO" " 데이터베이스: ${db_count}개"
log "INFO" " 일간: ${daily_count}개, 주간: ${weekly_count}개, 월간: ${monthly_count}개, 긴급: ${emergency_count}개"
local total_size=$(du -sh /backup/data 2>/dev/null | cut -f1)
log "INFO" "사용 공간: ${total_size}"
# 🆕 최근 백업 파일 정보
local latest_db=$(find "$DB_BACKUP_DIR" -name "*.sql.gz" -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -1 | cut -d' ' -f2-)
if [[ -n "$latest_db" ]]; then
local latest_db_date=$(stat -c %y "$latest_db" | cut -d'.' -f1)
log "INFO" "최근 DB 백업: $(basename "$latest_db") ($latest_db_date)"
fi
}
# 메인 루프
main_loop() {
initialize
local next_backup_time=$(calculate_next_backup)
local next_cleanup_time=$(calculate_next_cleanup)
local last_status_report=0
log "INFO" "첫 번째 백업 예정: $(TZ=Asia/Seoul date -d @$next_backup_time) ($(predict_backup_type $next_backup_time))"
log "INFO" "첫 번째 정리 예정: $(TZ=Asia/Seoul date -d @$next_cleanup_time)"
# 🆕 초기 상태 리포트
status_report
while true; do
local current_time=$(date +%s)
# 백업 시간 체크
if [[ $current_time -ge $next_backup_time ]]; then
run_backup
next_backup_time=$(calculate_next_backup)
log "INFO" "다음 백업 예정: $(TZ=Asia/Seoul date -d @$next_backup_time) ($(predict_backup_type $next_backup_time))"
fi
# 정리 시간 체크
if [[ $current_time -ge $next_cleanup_time ]]; then
run_cleanup
next_cleanup_time=$(calculate_next_cleanup)
log "INFO" "다음 정리 예정: $(TZ=Asia/Seoul date -d @$next_cleanup_time)"
fi
# 1시간마다 상태 리포트
if [[ $((current_time - last_status_report)) -ge 3600 ]]; then
status_report
last_status_report=$current_time
fi
# 1분마다 체크
sleep 60
done
}
# 시작
main_loop
EOF
backup-daemon.sh 는 Dockerfile에 의해 실행되며, main_loop를 통해 반복적으로 실행되어 백업 및 정리 등을 수행한다.
6. 백업 스크립트 생성 (backup.sh)
cat > /volume1/docker/NestJS_SNS/backup/backup.sh << 'EOF'
#!/bin/bash
# ==============================================
# 🗃️ 모든 백업 타입에 대응하는 백업 시스템
# ==============================================
# 더 안전한 설정
set -euo pipefail
# 설정 파일 로드
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/backup-config.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "❌ 설정 파일을 찾을 수 없습니다: $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
# 로그 디렉토리 생성
mkdir -p "$LOG_DIR"
# 로그 함수
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# Slack 알림 함수
send_slack_notification() {
local message="$1"
if [[ -z "${SLACK_WEBHOOK_URL:-}" ]]; then
log "WARN" "Slack Webhook URL이 설정되지 않음 - 알림 스킵"
return 0
fi
local payload="{\"text\": \"$message\"}"
if curl -s -X POST -H 'Content-type: application/json' \
--data "$payload" \
"$SLACK_WEBHOOK_URL" >/dev/null 2>&1; then
log "INFO" "📱 Slack 알림 전송 완료"
else
log "WARN" "📱 Slack 알림 전송 실패"
fi
}
# 백업 타입별 이모지 반환 함수
get_backup_emoji() {
local backup_type="$1"
case "$backup_type" in
"daily") echo "📅" ;;
"weekly") echo "📆" ;;
"monthly") echo "🗓️" ;;
"emergency") echo "🚨" ;;
*) echo "📦" ;;
esac
}
# 백업 타입별 표시명 반환 함수
get_backup_display_name() {
local backup_type="$1"
case "$backup_type" in
"daily") echo "Daily" ;;
"weekly") echo "Weekly" ;;
"monthly") echo "Monthly" ;;
"emergency") echo "Emergency" ;;
*) echo "Unknown" ;;
esac
}
# 백업 타입 자동 결정 함수
determine_backup_type() {
local day_of_week=$(date +%u)
local day_of_month=$(date +%d)
if [[ "$day_of_month" == "01" ]]; then
echo "monthly"
elif [[ "$day_of_week" == "7" ]]; then
echo "weekly"
else
echo "daily"
fi
}
# 메인 백업 함수
main_backup() {
local backup_type="${1:-emergency}"
# auto 타입인 경우 자동 결정
if [[ "$backup_type" == "auto" ]]; then
backup_type=$(determine_backup_type)
log "INFO" "자동 백업 타입 결정: $backup_type"
fi
local timestamp=$(date '+%Y%m%d_%H%M%S')
# 🆕 백업 타입별 이모지와 표시명
local backup_emoji=$(get_backup_emoji "$backup_type")
local backup_display_name=$(get_backup_display_name "$backup_type")
log "INFO" "=== Docker 컨테이너 백업 시작 ($backup_type) ==="
log "INFO" "컨테이너 ID: $(hostname)"
log "INFO" "타임스탬프: $timestamp"
# 🔔 동적 Slack 시작 알림
local start_message="🚀 $backup_emoji **$backup_display_name 백업 시작**
시작 시간: $(date '+%Y-%m-%d %H:%M:%S')
컨테이너: $(hostname)
타임스탬프: $timestamp"
send_slack_notification "$start_message"
# 백업 디렉토리 결정
local backup_dir=""
case "$backup_type" in
"daily") backup_dir="$DAILY_BACKUP_DIR" ;;
"weekly") backup_dir="$WEEKLY_BACKUP_DIR" ;;
"monthly") backup_dir="$MONTHLY_BACKUP_DIR" ;;
"emergency") backup_dir="$EMERGENCY_BACKUP_DIR" ;;
*) backup_dir="$EMERGENCY_BACKUP_DIR" ;;
esac
# 백업 디렉토리 생성
mkdir -p "$backup_dir"
mkdir -p "$DB_BACKUP_DIR"
# 카운터 초기화 (안전한 방법)
local success_count=0
local failed_count=0
local backup_results=""
# =====================================
# 1. 데이터베이스 백업
# =====================================
log "INFO" "🗃️ 데이터베이스 백업 시작..."
local db_backup_file="${DB_BACKUP_DIR}/db_${backup_type}_${timestamp}.sql"
local db_backup_gz="${db_backup_file}.gz"
if docker exec "$DB_CONTAINER_NAME" pg_isready -U "$DB_USER" >/dev/null 2>&1; then
if docker exec "$DB_CONTAINER_NAME" pg_dump -U "$DB_USER" -d "$DB_NAME" > "$db_backup_file" 2>/dev/null; then
if [[ -s "$db_backup_file" ]]; then
gzip "$db_backup_file"
if [[ -f "$db_backup_gz" ]]; then
local db_size=$(du -h "$db_backup_gz" | cut -f1)
log "INFO" "✅ 데이터베이스 백업 성공: $(basename "$db_backup_gz") ($db_size)"
backup_results+="✅ DB: $(basename "$db_backup_gz") ($db_size)\n"
success_count=$((success_count + 1))
log "INFO" "DB 백업 성공 - 현재 성공 카운트: $success_count"
else
log "ERROR" "❌ 데이터베이스 압축 실패"
backup_results+="❌ DB: 압축 실패\n"
failed_count=$((failed_count + 1))
fi
else
log "ERROR" "❌ 데이터베이스 백업 파일이 비어있음"
backup_results+="❌ DB: 빈 파일\n"
failed_count=$((failed_count + 1))
rm -f "$db_backup_file" 2>/dev/null || true
fi
else
log "ERROR" "❌ 데이터베이스 덤프 실패"
backup_results+="❌ DB: 덤프 실패\n"
failed_count=$((failed_count + 1))
fi
else
log "ERROR" "❌ 데이터베이스에 연결할 수 없음"
backup_results+="❌ DB: 연결 실패\n"
failed_count=$((failed_count + 1))
fi
log "INFO" "=== 데이터베이스 백업 완료. 소스코드 백업 시작 ==="
# =====================================
# 2. 소스 코드 백업
# =====================================
log "INFO" "📦 소스 코드 백업 시작..."
local source_backup_file="${backup_dir}/source_${backup_type}_${timestamp}.tar.gz"
# PROJECT_ROOT 확인
if [[ -d "$PROJECT_ROOT" ]]; then
log "INFO" "프로젝트 루트 디렉토리 확인됨: $PROJECT_ROOT"
# 현재 디렉토리 저장
local original_dir=$(pwd)
# PROJECT_ROOT로 이동
if cd "$PROJECT_ROOT" 2>/dev/null; then
log "INFO" "프로젝트 루트로 이동 성공: $(pwd)"
# tar 명령어 실행 (set +e로 임시 오류 무시)
set +e
tar --exclude='node_modules' \
--exclude='.git' \
--exclude='dist' \
--exclude='coverage' \
--exclude='*.log' \
--exclude='backup/data' \
-czf "$source_backup_file" \
. 2>/dev/null
local tar_exit_code=$?
set -e
if [[ $tar_exit_code -eq 0 ]] && [[ -f "$source_backup_file" ]] && [[ -s "$source_backup_file" ]]; then
local source_size=$(du -h "$source_backup_file" | cut -f1)
log "INFO" "✅ 소스 코드 백업 성공: $(basename "$source_backup_file") ($source_size)"
backup_results+="✅ 소스코드: $(basename "$source_backup_file") ($source_size)\n"
success_count=$((success_count + 1))
log "INFO" "소스코드 백업 성공 - 현재 성공 카운트: $success_count"
else
log "ERROR" "❌ 소스 코드 백업 실패 (tar exit code: $tar_exit_code)"
backup_results+="❌ 소스코드: 백업 실패\n"
failed_count=$((failed_count + 1))
rm -f "$source_backup_file" 2>/dev/null || true
fi
# 원래 디렉토리로 복귀
cd "$original_dir"
else
log "ERROR" "❌ 프로젝트 루트로 이동 실패: $PROJECT_ROOT"
backup_results+="❌ 소스코드: 디렉토리 이동 실패\n"
failed_count=$((failed_count + 1))
fi
else
log "ERROR" "❌ 프로젝트 루트 디렉토리를 찾을 수 없음: $PROJECT_ROOT"
backup_results+="❌ 소스코드: 디렉토리 없음\n"
failed_count=$((failed_count + 1))
fi
log "INFO" "=== 소스 코드 백업 완료. Docker 설정 백업 시작 ==="
# =====================================
# 3. Docker 설정 백업
# =====================================
log "INFO" "🐳 Docker 설정 백업 시작..."
local docker_backup_file="${backup_dir}/docker_${backup_type}_${timestamp}.tar.gz"
local temp_docker_dir="/tmp/docker_backup_${timestamp}"
mkdir -p "$temp_docker_dir"
local docker_files_found=0
# Docker Compose 파일 복사
if [[ -f "$DOCKER_COMPOSE_FILE" ]]; then
cp "$DOCKER_COMPOSE_FILE" "$temp_docker_dir/" 2>/dev/null || true
docker_files_found=$((docker_files_found + 1))
log "INFO" "Docker Compose 파일 복사됨"
else
log "WARN" "Docker Compose 파일 없음: $DOCKER_COMPOSE_FILE"
fi
# 환경 변수 파일 복사
if [[ -f "$ENV_FILE" ]]; then
cp "$ENV_FILE" "$temp_docker_dir/" 2>/dev/null || true
docker_files_found=$((docker_files_found + 1))
log "INFO" "환경 변수 파일 복사됨"
else
log "WARN" "환경 변수 파일 없음: $ENV_FILE"
fi
# 컨테이너 정보 저장
{
echo "=== Backup Info ==="
echo "Backup Time: $(date)"
echo "Backup Type: $backup_type"
echo "Container ID: $(hostname)"
echo ""
echo "=== Running Containers ==="
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null || echo "Docker 명령어 실행 실패"
} > "$temp_docker_dir/backup_info.txt"
docker_files_found=$((docker_files_found + 1))
# 압축
if [[ $docker_files_found -gt 0 ]]; then
set +e
tar -czf "$docker_backup_file" -C "/tmp" "docker_backup_${timestamp}" 2>/dev/null
local docker_tar_exit_code=$?
set -e
if [[ $docker_tar_exit_code -eq 0 ]] && [[ -f "$docker_backup_file" ]] && [[ -s "$docker_backup_file" ]]; then
local docker_size=$(du -h "$docker_backup_file" | cut -f1)
log "INFO" "✅ Docker 설정 백업 성공: $(basename "$docker_backup_file") ($docker_size)"
backup_results+="✅ Docker: $(basename "$docker_backup_file") ($docker_size)\n"
success_count=$((success_count + 1))
log "INFO" "Docker 백업 성공 - 현재 성공 카운트: $success_count"
else
log "ERROR" "❌ Docker 설정 백업 실패"
backup_results+="❌ Docker: 백업 실패\n"
failed_count=$((failed_count + 1))
rm -f "$docker_backup_file" 2>/dev/null || true
fi
else
log "WARN" "⚠️ Docker 설정 파일을 찾을 수 없음"
backup_results+="⚠️ Docker: 파일 없음\n"
failed_count=$((failed_count + 1))
fi
# 임시 디렉토리 정리
rm -rf "$temp_docker_dir" 2>/dev/null || true
log "INFO" "=== Docker 설정 백업 완료. 결과 요약 시작 ==="
# =====================================
# 4. 결과 요약 및 동적 알림
# =====================================
log "INFO" "=== 백업 결과 요약 ==="
log "INFO" "✅ 성공한 백업: ${success_count}개"
log "INFO" "❌ 실패한 백업: ${failed_count}개"
# 백업 파일 목록
log "INFO" "=== 생성된 백업 파일 ==="
for dir in "$backup_dir" "$DB_BACKUP_DIR"; do
if [[ -d "$dir" ]]; then
find "$dir" -name "*${timestamp}*" -type f 2>/dev/null | while read file; do
if [[ -f "$file" ]]; then
local size=$(du -h "$file" | cut -f1)
log "INFO" "📁 $(basename "$file") ($size)"
fi
done
fi
done
# 🔔 동적 Slack 완료 알림
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local total_backups=$((success_count + failed_count))
if [[ $failed_count -eq 0 ]]; then
log "INFO" "🎉 === 모든 백업 완료 (성공) ==="
local success_msg="🎉 $backup_emoji **$backup_display_name 백업 완료 (성공)**
✅ 성공: ${success_count}/${total_backups}개
완료 시간: $end_time
타임스탬프: $timestamp
$backup_results"
send_slack_notification "$success_msg"
else
log "WARN" "⚠️ === 백업 완료 (일부 실패) ==="
local warning_msg="⚠️ $backup_emoji **$backup_display_name 백업 완료 (일부 실패)**
✅ 성공: ${success_count}개, ❌ 실패: ${failed_count}개
완료 시간: $end_time
타임스탬프: $timestamp
$backup_results"
send_slack_notification "$warning_msg"
fi
log "INFO" "백업 프로세스 완전 종료"
# 최종 결과 반환
if [[ $failed_count -eq 0 ]]; then
return 0
else
return 1
fi
}
# 사용법 출력
usage() {
echo "🗃️ Docker 백업 시스템"
echo ""
echo "사용법: $0 [backup_type]"
echo ""
echo "백업 타입:"
echo " auto - 자동 백업 타입 결정"
echo " daily - 일간 백업"
echo " weekly - 주간 백업"
echo " monthly - 월간 백업"
echo " emergency - 긴급 백업 (기본값)"
}
# 메인 실행
case "${1:-emergency}" in
"auto"|"daily"|"weekly"|"monthly"|"emergency")
main_backup "$1"
exit_code=$?
exit $exit_code
;;
"-h"|"--help")
usage
;;
*)
echo "❌잘못된 인수: $1"
usage
exit 1
;;
esac
EOF
backup-daemon.sh 이 실행되면서 백업 시간이 되면 run_backup()을 수행하여 backup.sh 를 실행한다.
backup.sh 는 백업을 수행하며, sns-dev-backup/backup/data/ 의 daily, weekly, monthly, emergency, database 등으로 백업된다.
backup_daily_20250704_170000.tar.gz
├── src/ # 소스코드
├── uploads/ # 업로드 파일
├── logs/ # 로그 파일
├── docker-compose.yml # 설정 파일
├── .env # 환경변수
└── database_dump.sql # DB 덤프
백업에 포함되는 내용물은 위와 같다.
위와 같이 백업 명령어 실행 시 소스코드, Docker 환경설정, 데이터베이스 등이 백업되며, 슬랙으로 알림을 보내준다.
7. 정리 스크립트 생성 (cleanup.sh)
cat > /volume1/docker/NestJS_SNS/backup/cleanup.sh << 'EOF'
#!/bin/bash
# ==============================================
# 🧹 백업 파일 정리 시스템 (Docker용)
# ==============================================
set -euo pipefail
# 설정 파일 로드
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/backup-config.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "❌ 설정 파일을 찾을 수 없습니다: $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
# Docker 환경 감지
if [[ -f /.dockerenv ]]; then
RUNNING_IN_DOCKER=true
BACKUP_DATA_DIR="/backup/data"
LOG_DIR="/backup/logs"
LOG_FILE="$LOG_DIR/backup.log"
else
RUNNING_IN_DOCKER=false
BACKUP_DATA_DIR="${PROJECT_ROOT}/backup/data"
LOG_DIR="${PROJECT_ROOT}/backup/logs"
LOG_FILE="$LOG_DIR/backup.log"
fi
# 로그 디렉토리 생성
mkdir -p "$LOG_DIR"
# 로그 함수
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# Slack 알림 함수
send_slack_notification() {
local message="$1"
local color="$2"
if [[ "$ENABLE_SLACK_NOTIFICATIONS" == "true" && -n "$SLACK_WEBHOOK_URL" ]]; then
local container_info=""
if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then
container_info="\n• 컨테이너: $(hostname)"
fi
curl -X POST -H 'Content-type: application/json' \
--data "{
\"text\": \"🧹 **백업 정리 시스템 알림**\",
\"attachments\": [{
\"color\": \"$color\",
\"text\": \"$message$container_info\",
\"footer\": \"$(if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then echo "Docker 백업 컨테이너"; else echo "백업 시스템"; fi)\",
\"ts\": $(date +%s)
}]
}" \
"$SLACK_WEBHOOK_URL" 2>/dev/null || true
fi
}
# 오래된 백업 정리
cleanup_old_backups() {
log "INFO" "=== 오래된 백업 정리 시작 ==="
log "INFO" "실행 환경: $(if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then echo "Docker 컨테이너 ($(hostname))"; else echo "호스트"; fi)"
local total_cleaned=0
local total_size_freed=0
# 백업 디렉토리 경로 설정
local daily_dir="${BACKUP_DATA_DIR}/daily"
local weekly_dir="${BACKUP_DATA_DIR}/weekly"
local monthly_dir="${BACKUP_DATA_DIR}/monthly"
local emergency_dir="${BACKUP_DATA_DIR}/emergency"
local db_dir="${BACKUP_DATA_DIR}/database"
# 일간 백업 정리
if [[ -d "$daily_dir" ]]; then
log "INFO" "일간 백업 정리 중 (${DAILY_RETENTION_DAYS}일 이상 된 파일)..."
# 삭제 전 크기 계산
local before_size=$(du -sb "$daily_dir" 2>/dev/null | cut -f1 || echo 0)
local cleaned_files=$(find "$daily_dir" -type f -mtime +$DAILY_RETENTION_DAYS -print0 | wc -l --files0-from=-)
find "$daily_dir" -type f -mtime +$DAILY_RETENTION_DAYS -delete 2>/dev/null || true
# 삭제 후 크기 계산
local after_size=$(du -sb "$daily_dir" 2>/dev/null | cut -f1 || echo 0)
local freed_size=$((before_size - after_size))
total_cleaned=$((total_cleaned + cleaned_files))
total_size_freed=$((total_size_freed + freed_size))
log "INFO" "일간 백업 정리 완료: ${cleaned_files}개 파일, $(numfmt --to=iec $freed_size)B 절약"
fi
# 주간 백업 정리
if [[ -d "$weekly_dir" ]]; then
log "INFO" "주간 백업 정리 중 (${WEEKLY_RETENTION_DAYS}일 이상 된 파일)..."
local before_size=$(du -sb "$weekly_dir" 2>/dev/null | cut -f1 || echo 0)
local cleaned_files=$(find "$weekly_dir" -type f -mtime +$WEEKLY_RETENTION_DAYS -print0 | wc -l --files0-from=-)
find "$weekly_dir" -type f -mtime +$WEEKLY_RETENTION_DAYS -delete 2>/dev/null || true
local after_size=$(du -sb "$weekly_dir" 2>/dev/null | cut -f1 || echo 0)
local freed_size=$((before_size - after_size))
total_cleaned=$((total_cleaned + cleaned_files))
total_size_freed=$((total_size_freed + freed_size))
log "INFO" "주간 백업 정리 완료: ${cleaned_files}개 파일, $(numfmt --to=iec $freed_size)B 절약"
fi
# 월간 백업 정리
if [[ -d "$monthly_dir" ]]; then
log "INFO" "월간 백업 정리 중 (${MONTHLY_RETENTION_DAYS}일 이상 된 파일)..."
local before_size=$(du -sb "$monthly_dir" 2>/dev/null | cut -f1 || echo 0)
local cleaned_files=$(find "$monthly_dir" -type f -mtime +$MONTHLY_RETENTION_DAYS -print0 | wc -l --files0-from=-)
find "$monthly_dir" -type f -mtime +$MONTHLY_RETENTION_DAYS -delete 2>/dev/null || true
local after_size=$(du -sb "$monthly_dir" 2>/dev/null | cut -f1 || echo 0)
local freed_size=$((before_size - after_size))
total_cleaned=$((total_cleaned + cleaned_files))
total_size_freed=$((total_size_freed + freed_size))
log "INFO" "월간 백업 정리 완료: ${cleaned_files}개 파일, $(numfmt --to=iec $freed_size)B 절약"
fi
# 긴급 백업 정리
if [[ -d "$emergency_dir" ]]; then
log "INFO" "긴급 백업 정리 중 (${EMERGENCY_RETENTION_DAYS}일 이상 된 파일)..."
local before_size=$(du -sb "$emergency_dir" 2>/dev/null | cut -f1 || echo 0)
local cleaned_files=$(find "$emergency_dir" -type f -mtime +$EMERGENCY_RETENTION_DAYS -print0 | wc -l --files0-from=-)
find "$emergency_dir" -type f -mtime +$EMERGENCY_RETENTION_DAYS -delete 2>/dev/null || true
local after_size=$(du -sb "$emergency_dir" 2>/dev/null | cut -f1 || echo 0)
local freed_size=$((before_size - after_size))
total_cleaned=$((total_cleaned + cleaned_files))
total_size_freed=$((total_size_freed + freed_size))
log "INFO" "긴급 백업 정리 완료: ${cleaned_files}개 파일, $(numfmt --to=iec $freed_size)B 절약"
fi
# 데이터베이스 백업 정리 (일간 백업 기준)
if [[ -d "$db_dir" ]]; then
log "INFO" "데이터베이스 백업 정리 중 (${DAILY_RETENTION_DAYS}일 이상 된 파일)..."
local before_size=$(du -sb "$db_dir" 2>/dev/null | cut -f1 || echo 0)
local cleaned_files=$(find "$db_dir" -type f -mtime +$DAILY_RETENTION_DAYS -print0 | wc -l --files0-from=-)
find "$db_dir" -type f -mtime +$DAILY_RETENTION_DAYS -delete 2>/dev/null || true
local after_size=$(du -sb "$db_dir" 2>/dev/null | cut -f1 || echo 0)
local freed_size=$((before_size - after_size))
total_cleaned=$((total_cleaned + cleaned_files))
total_size_freed=$((total_size_freed + freed_size))
log "INFO" "데이터베이스 백업 정리 완료: ${cleaned_files}개 파일, $(numfmt --to=iec $freed_size)B 절약"
fi
# 빈 디렉토리 정리
find "$BACKUP_DATA_DIR" -type d -empty -delete 2>/dev/null || true
log "INFO" "=== 백업 정리 완료: 총 ${total_cleaned}개 파일, $(numfmt --to=iec $total_size_freed)B 절약 ==="
# Slack 알림
if [[ $total_cleaned -gt 0 ]]; then
send_slack_notification "🧹 **백업 정리 완료**\n• 정리된 파일: ${total_cleaned}개\n• 절약된 공간: $(numfmt --to=iec $total_size_freed)B" "good"
fi
}
# 로그 파일 정리
cleanup_logs() {
log "INFO" "=== 로그 파일 정리 시작 ==="
if [[ -d "$LOG_DIR" ]]; then
# 오래된 로그 파일 정리
local cleaned_logs=$(find "$LOG_DIR" -name "*.log" -not -name "$(basename "$LOG_FILE")" -mtime +$LOG_RETENTION_DAYS -print | wc -l)
find "$LOG_DIR" -name "*.log" -not -name "$(basename "$LOG_FILE")" -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null || true
log "INFO" "오래된 로그 파일 정리: ${cleaned_logs}개"
# 큰 로그 파일 회전
if [[ -f "$LOG_FILE" ]]; then
local log_size=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
local max_bytes=$(echo "$MAX_LOG_SIZE" | sed 's/[^0-9]*//g')000000 # MB to bytes
if [[ $log_size -gt $max_bytes ]]; then
log "INFO" "로그 파일 회전 중 (크기: $(numfmt --to=iec $log_size)B)"
# 로그 파일 회전
local timestamp=$(date +%Y%m%d_%H%M%S)
cp "$LOG_FILE" "${LOG_FILE}.${timestamp}"
# 새 로그 파일 시작
cat > "$LOG_FILE" << LOG_HEADER
# ==============================================
# 🔄 백업 시스템 로그 (회전됨)
# ==============================================
# 이전 로그: ${LOG_FILE}.${timestamp}
# 회전 시간: $(date)
# 회전 이유: 크기 초과 ($(numfmt --to=iec $log_size)B > $(echo "$MAX_LOG_SIZE"))
# ==============================================
LOG_HEADER
log "INFO" "로그 파일 회전 완료: ${LOG_FILE}.${timestamp}"
# 오래된 회전 로그 정리
find "$LOG_DIR" -name "$(basename "$LOG_FILE").*" -mtime +7 -delete 2>/dev/null || true
fi
fi
# 로그 압축 (선택적)
find "$LOG_DIR" -name "*.log.*" -not -name "*.gz" -mtime +1 -exec gzip {} \; 2>/dev/null || true
fi
log "INFO" "=== 로그 파일 정리 완료 ==="
}
# 임시 파일 정리
cleanup_temp_files() {
log "INFO" "=== 임시 파일 정리 시작 ==="
local temp_cleaned=0
# /tmp의 백업 관련 임시 파일 정리
if [[ -d "/tmp" ]]; then
temp_cleaned=$(find /tmp -name "*backup*" -o -name "*restore*" -mtime +1 -type f 2>/dev/null | wc -l)
find /tmp -name "*backup*" -o -name "*restore*" -mtime +1 -type f -delete 2>/dev/null || true
fi
# Docker 컨테이너 내부 임시 파일
if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then
# 컨테이너 내부 임시 파일 정리
local container_temp=$(find /backup -name "*.tmp" -o -name "*.temp" -mtime +1 -type f 2>/dev/null | wc -l)
find /backup -name "*.tmp" -o -name "*.temp" -mtime +1 -type f -delete 2>/dev/null || true
temp_cleaned=$((temp_cleaned + container_temp))
# 컨테이너 내부 캐시 정리
if command -v npm >/dev/null 2>&1; then
npm cache clean --force >/dev/null 2>&1 || true
fi
fi
log "INFO" "임시 파일 정리 완료: ${temp_cleaned}개"
log "INFO" "=== 임시 파일 정리 완료 ==="
}
# 디스크 사용량 체크
check_disk_usage() {
log "INFO" "=== 디스크 사용량 체크 ==="
# 백업 디렉토리의 디스크 사용량 확인
local usage=""
local mount_point=""
if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then
# Docker 볼륨의 사용량
mount_point="/backup"
usage=$(df "$mount_point" 2>/dev/null | awk 'NR==2 {print $5}' | sed 's/%//' || echo "0")
else
# 호스트의 사용량
mount_point="$BACKUP_DATA_DIR"
usage=$(df "$mount_point" 2>/dev/null | awk 'NR==2 {print $5}' | sed 's/%//' || echo "0")
fi
log "INFO" "현재 디스크 사용률: ${usage}% (마운트: $mount_point)"
# 백업 디렉토리 크기 정보
local backup_size=$(du -sh "$BACKUP_DATA_DIR" 2>/dev/null | cut -f1 || echo "알 수 없음")
log "INFO" "백업 데이터 크기: $backup_size"
if [[ $usage -gt 90 ]]; then
log "WARN" "🚨 디스크 사용률이 90%를 초과했습니다!"
# 추가 정리 (긴급 상황)
log "INFO" "긴급 정리 실행 중..."
# 더 짧은 보존 기간으로 추가 정리
local emergency_cleaned=0
# 일간 백업을 3일로 단축
if [[ -d "${BACKUP_DATA_DIR}/daily" ]]; then
local daily_emergency=$(find "${BACKUP_DATA_DIR}/daily" -type f -mtime +3 -print | wc -l)
find "${BACKUP_DATA_DIR}/daily" -type f -mtime +3 -delete 2>/dev/null || true
emergency_cleaned=$((emergency_cleaned + daily_emergency))
fi
# 주간 백업을 14일로 단축
if [[ -d "${BACKUP_DATA_DIR}/weekly" ]]; then
local weekly_emergency=$(find "${BACKUP_DATA_DIR}/weekly" -type f -mtime +14 -print | wc -l)
find "${BACKUP_DATA_DIR}/weekly" -type f -mtime +14 -delete 2>/dev/null || true
emergency_cleaned=$((emergency_cleaned + weekly_emergency))
fi
# 긴급 백업을 7일로 단축
if [[ -d "${BACKUP_DATA_DIR}/emergency" ]]; then
local emergency_backup_cleaned=$(find "${BACKUP_DATA_DIR}/emergency" -type f -mtime +7 -print | wc -l)
find "${BACKUP_DATA_DIR}/emergency" -type f -mtime +7 -delete 2>/dev/null || true
emergency_cleaned=$((emergency_cleaned + emergency_backup_cleaned))
fi
log "WARN" "긴급 정리로 ${emergency_cleaned}개 파일 추가 삭제"
# 정리 후 사용량 재확인
local new_usage=$(df "$mount_point" 2>/dev/null | awk 'NR==2 {print $5}' | sed 's/%//' || echo "0")
log "INFO" "정리 후 디스크 사용률: ${new_usage}%"
# Slack 긴급 알림
send_slack_notification "🚨 **디스크 공간 부족 경고**\n• 사용률: ${usage}% → ${new_usage}%\n• 긴급 정리: ${emergency_cleaned}개 파일 삭제\n• 마운트: $mount_point" "danger"
elif [[ $usage -gt 80 ]]; then
log "WARN" "⚠️ 디스크 사용률이 80%를 초과했습니다. 주의가 필요합니다."
send_slack_notification "⚠️ **디스크 사용률 경고**\n• 현재 사용률: ${usage}%\n• 백업 크기: $backup_size\n• 마운트: $mount_point" "warning"
fi
# 백업 디렉토리별 사용량
echo ""
echo "📊 백업 디렉토리별 사용량:"
if [[ -d "$BACKUP_DATA_DIR" ]]; then
du -sh "$BACKUP_DATA_DIR"/* 2>/dev/null | sort -hr || echo " 백업 디렉토리가 비어있습니다"
else
echo " 백업 디렉토리를 찾을 수 없습니다: $BACKUP_DATA_DIR"
fi
# Docker 특화 정보
if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then
echo ""
echo "🐳 Docker 컨테이너 정보:"
echo " 컨테이너 ID: $(hostname)"
echo " 컨테이너 이미지: $(cat /etc/hostname 2>/dev/null || echo 'unknown')"
echo " 메모리 사용량: $(free -h | awk 'NR==2{printf "%.1f%%", $3*100/$2 }')"
fi
}
# 백업 상태 리포트 생성
generate_status_report() {
local report_file="${LOG_DIR}/backup_status_$(date +%Y%m%d).txt"
{
echo "=== 백업 시스템 상태 리포트 ==="
echo "생성 시간: $(date)"
echo "실행 환경: $(if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then echo "Docker 컨테이너 ($(hostname))"; else echo "호스트"; fi)"
echo ""
echo "=== 디렉토리별 백업 개수 ==="
echo "일간 백업: $(find "${BACKUP_DATA_DIR}/daily" -name "*.tar.gz" 2>/dev/null | wc -l)개"
echo "주간 백업: $(find "${BACKUP_DATA_DIR}/weekly" -name "*.tar.gz" 2>/dev/null | wc -l)개"
echo "월간 백업: $(find "${BACKUP_DATA_DIR}/monthly" -name "*.tar.gz" 2>/dev/null | wc -l)개"
echo "긴급 백업: $(find "${BACKUP_DATA_DIR}/emergency" -name "*.tar.gz" 2>/dev/null | wc -l)개"
echo "DB 백업: $(find "${BACKUP_DATA_DIR}/database" -name "*.sql.gz" 2>/dev/null | wc -l)개"
echo ""
echo "=== 디렉토리별 용량 ==="
if [[ -d "$BACKUP_DATA_DIR" ]]; then
du -sh "$BACKUP_DATA_DIR"/* 2>/dev/null | sort -hr || echo "백업 디렉토리가 비어있습니다"
else
echo "백업 디렉토리를 찾을 수 없습니다"
fi
echo ""
echo "=== 최근 백업 파일 (각 타입별 최신 3개) ==="
for type in daily weekly monthly emergency; do
echo ""
echo "$type 백업:"
if [[ -d "${BACKUP_DATA_DIR}/$type" ]]; then
find "${BACKUP_DATA_DIR}/$type" -name "*.tar.gz" -exec ls -lh {} \; 2>/dev/null | sort -k6,7 -r | head -3 | awk '{print " " $9 " (" $5 ", " $6 " " $7 ")"}'
else
echo " 없음"
fi
done
echo ""
echo "=== 시스템 정보 ==="
echo "호스트명: $(hostname)"
if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then
echo "실행 환경: Docker 컨테이너"
echo "가용 공간: $(df -h /backup | awk 'NR==2 {print $4}')"
echo "사용률: $(df -h /backup | awk 'NR==2 {print $5}')"
echo "메모리 사용량: $(free -h | awk 'NR==2{printf "%.1f%%", $3*100/$2 }')"
else
echo "실행 환경: 호스트"
echo "가용 공간: $(df -h "$BACKUP_DATA_DIR" | awk 'NR==2 {print $4}')"
echo "사용률: $(df -h "$BACKUP_DATA_DIR" | awk 'NR==2 {print $5}')"
fi
echo ""
echo "=== 정리 설정 ==="
echo "일간 백업 보존: ${DAILY_RETENTION_DAYS}일"
echo "주간 백업 보존: ${WEEKLY_RETENTION_DAYS}일"
echo "월간 백업 보존: ${MONTHLY_RETENTION_DAYS}일"
echo "긴급 백업 보존: ${EMERGENCY_RETENTION_DAYS}일"
echo "로그 보존: ${LOG_RETENTION_DAYS}일"
} > "$report_file"
log "INFO" "상태 리포트 생성: $report_file"
# 리포트 내용을 로그에도 기록
echo ""
echo "📊 === 백업 시스템 요약 ==="
echo "총 백업 파일: $(find "$BACKUP_DATA_DIR" -name "*.tar.gz" -o -name "*.sql.gz" 2>/dev/null | wc -l)개"
echo "총 백업 크기: $(du -sh "$BACKUP_DATA_DIR" 2>/dev/null | cut -f1 || echo "알 수 없음")"
echo "상태 리포트: $report_file"
}
# 메인 정리 함수
main_cleanup() {
log "INFO" "=== 백업 시스템 정리 시작 ==="
log "INFO" "실행 환경: $(if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then echo "Docker 컨테이너 ($(hostname))"; else echo "호스트"; fi)"
# 정리 시작 알림
send_slack_notification "🧹 **백업 시스템 정리 시작**\n• 시간: $(date)" "good"
cleanup_old_backups
cleanup_logs
cleanup_temp_files
check_disk_usage
generate_status_report
log "INFO" "=== 백업 시스템 정리 완료 ==="
# 정리 완료 알림
local total_backups=$(find "$BACKUP_DATA_DIR" -name "*.tar.gz" -o -name "*.sql.gz" 2>/dev/null | wc -l)
local total_size=$(du -sh "$BACKUP_DATA_DIR" 2>/dev/null | cut -f1 || echo "알 수 없음")
send_slack_notification "✅ **백업 시스템 정리 완료**\n• 총 백업: ${total_backups}개\n• 총 크기: $total_size\n• 완료 시간: $(date)" "good"
}
# Docker 환경 안내
show_docker_info() {
if [[ "$RUNNING_IN_DOCKER" == "true" ]]; then
echo ""
echo "🐳 Docker 컨테이너에서 실행 중"
echo " 컨테이너 ID: $(hostname)"
echo " 백업 데이터: $BACKUP_DATA_DIR"
echo " 로그 위치: $LOG_DIR"
echo ""
fi
}
# 사용법 출력
usage() {
show_docker_info
echo "사용법: $0 [command]"
echo ""
echo "명령어:"
echo " cleanup - 전체 정리 실행 (기본값)"
echo " backups - 오래된 백업만 정리"
echo " logs - 로그 파일만 정리"
echo " temp - 임시 파일만 정리"
echo " status - 상태 리포트만 생성"
echo " check - 디스크 사용량만 체크"
echo ""
echo "예시:"
echo " $0 # 전체 정리"
echo " $0 backups # 백업 파일만 정리"
echo " $0 status # 상태 리포트 생성"
echo ""
echo "Docker 환경에서 실행 방법:"
echo " docker exec sns-dev-backup /backup/scripts/cleanup.sh"
echo " docker exec sns-dev-backup /backup/scripts/cleanup.sh status"
}
# 메인 실행
case "${1:-cleanup}" in
"cleanup")
main_cleanup
;;
"backups")
cleanup_old_backups
;;
"logs")
cleanup_logs
;;
"temp")
cleanup_temp_files
;;
"status")
generate_status_report
;;
"check")
check_disk_usage
;;
"help"|"-h"|"--help")
usage
;;
*)
echo "❌ 알 수 없는 명령어: $1"
usage
exit 1
;;
esac
EOF
cleanup.sh 스크립트는 backup-deamon.sh 에서 정리 시간이 되었을 때 run_cleanup() 함수가 실행되며 호출된다.
오래된 백업들을 정리하며, 리포트를 생성한다.
8. 복원 스크립트 생성 (restore.sh)
cat > /volume1/docker/NestJS_SNS/backup/restore.sh << 'EOF'
#!/bin/bash
# ==============================================
# 🔄 백업 복원 시스템 (완전판)
# ==============================================
set -euo pipefail
# 설정 파일 로드
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/backup-config.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "❌ 설정 파일을 찾을 수 없습니다: $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
# 로그 디렉토리 생성
mkdir -p "$LOG_DIR"
# 로그 함수
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# 🆕 백업 파일 목록 표시
list_backups() {
log "INFO" "=== 사용 가능한 백업 파일 목록 ==="
# 📊 데이터베이스 백업
log "INFO" "📊 데이터베이스 백업:"
if [[ -d "$DB_BACKUP_DIR" ]]; then
find "$DB_BACKUP_DIR" -name "*.sql.gz" -type f 2>/dev/null | sort -r | while read file; do
if [[ -f "$file" ]]; then
local size=$(du -h "$file" | cut -f1)
local date=$(stat -c %y "$file" | cut -d' ' -f1,2 | cut -d'.' -f1)
log "INFO" " $(basename "$file") ($date)"
fi
done
local db_count=$(find "$DB_BACKUP_DIR" -name "*.sql.gz" -type f 2>/dev/null | wc -l)
[[ $db_count -eq 0 ]] && log "INFO" " (백업 파일 없음)"
else
log "INFO" " (백업 디렉토리 없음)"
fi
echo ""
# 📁 백업 타입별 목록
for backup_type in emergency daily weekly monthly; do
local backup_dir=""
case "$backup_type" in
"daily") backup_dir="$DAILY_BACKUP_DIR" ;;
"weekly") backup_dir="$WEEKLY_BACKUP_DIR" ;;
"monthly") backup_dir="$MONTHLY_BACKUP_DIR" ;;
"emergency") backup_dir="$EMERGENCY_BACKUP_DIR" ;;
esac
if [[ -d "$backup_dir" ]]; then
local file_count=$(find "$backup_dir" -name "*.tar.gz" -type f 2>/dev/null | wc -l)
if [[ $file_count -gt 0 ]]; then
log "INFO" "📁 ${backup_type^} 백업 (${file_count}개):"
# 소스 코드 백업
find "$backup_dir" -name "source_*.tar.gz" -type f 2>/dev/null | sort -r | head -5 | while read file; do
if [[ -f "$file" ]]; then
local size=$(du -h "$file" | cut -f1)
local date=$(stat -c %y "$file" | cut -d' ' -f1,2 | cut -d'.' -f1)
log "INFO" " 📦 $(basename "$file") ($size, $date)"
fi
done
# Docker 설정 백업
find "$backup_dir" -name "docker_*.tar.gz" -type f 2>/dev/null | sort -r | head -3 | while read file; do
if [[ -f "$file" ]]; then
local size=$(du -h "$file" | cut -f1)
local date=$(stat -c %y "$file" | cut -d' ' -f1,2 | cut -d'.' -f1)
log "INFO" " 🐳 $(basename "$file") ($size, $date)"
fi
done
echo ""
fi
fi
done
# 📈 전체 백업 통계
local total_size=$(du -sh "$BACKUP_BASE_DIR" 2>/dev/null | cut -f1 || echo "0")
local total_files=$(find "$BACKUP_BASE_DIR" -type f 2>/dev/null | wc -l)
log "INFO" "📈 전체 백업 통계:"
log "INFO" " 총 파일 수: ${total_files}개"
log "INFO" " 총 사용 용량: ${total_size}"
echo ""
log "INFO" "💡 복원 명령어 예시:"
log "INFO" " 데이터베이스: $0 partial db /backup/data/database/[파일명]"
log "INFO" " 소스 코드: $0 partial source /backup/data/[타입]/[파일명]"
log "INFO" " Docker 설정: $0 partial docker /backup/data/[타입]/[파일명]"
}
# 🆕 상세 백업 정보 표시
show_backup_details() {
local backup_file="$1"
if [[ ! -f "$backup_file" ]]; then
log "ERROR" "백업 파일을 찾을 수 없음: $backup_file"
return 1
fi
log "INFO" "=== 백업 파일 상세 정보 ==="
log "INFO" "파일 경로: $backup_file"
log "INFO" "파일 크기: $(du -h "$backup_file" | cut -f1)"
log "INFO" "생성 날짜: $(stat -c %y "$backup_file" | cut -d'.' -f1)"
log "INFO" "파일 타입: $(file "$backup_file")"
# 압축 파일 내용 미리보기
if [[ "$backup_file" == *.tar.gz ]]; then
log "INFO" "압축 파일 내용:"
tar -tzf "$backup_file" 2>/dev/null | head -10 | while read line; do
log "INFO" " $line"
done
local total_files=$(tar -tzf "$backup_file" 2>/dev/null | wc -l)
log "INFO" " ... (총 ${total_files}개 파일)"
elif [[ "$backup_file" == *.sql.gz ]]; then
log "INFO" "SQL 파일 정보:"
gunzip -c "$backup_file" | head -20 | grep -E "^(CREATE|INSERT|COPY)" | head -5 | while read line; do
log "INFO" " $line"
done
fi
}
# 안전한 데이터베이스 복원 함수
restore_database() {
local backup_file="$1"
log "INFO" "데이터베이스 복원 시작: $backup_file"
# 1. 백업 파일 검증
if [[ ! -f "$backup_file" ]]; then
log "ERROR" "백업 파일을 찾을 수 없음: $backup_file"
return 1
fi
# 2. 백업 파일 압축 해제
local sql_file="$backup_file"
local cleanup_sql=false
if [[ "$backup_file" == *.gz ]]; then
sql_file="/tmp/restore_$(basename "$backup_file" .gz)"
log "INFO" "백업 파일 압축 해제 중..."
if ! gunzip -c "$backup_file" > "$sql_file"; then
log "ERROR" "백업 파일 압축 해제 실패"
return 1
fi
cleanup_sql=true
fi
# 3. 모든 연결 강제 종료
log "INFO" "데이터베이스 연결 종료 중..."
for i in {1..3}; do
docker exec "$DB_CONTAINER_NAME" psql -U "$DB_USER" -d postgres -c "
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = '$DB_NAME'
AND pid <> pg_backend_pid();" 2>/dev/null || true
sleep 2
done
# 4. 기존 데이터베이스의 모든 테이블/스키마 삭제
log "INFO" "기존 데이터 정리 중..."
docker exec "$DB_CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -c "
-- 모든 테이블 삭제
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO public;
" 2>/dev/null || true
# 5. 새로운 데이터 복원
log "INFO" "새로운 데이터 복원 중..."
if docker exec -i "$DB_CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" < "$sql_file" 2>/dev/null; then
log "INFO" "✅ 데이터베이스 복원 완료"
# 정리
[[ "$cleanup_sql" == true ]] && rm -f "$sql_file"
return 0
else
log "ERROR" "❌ 데이터베이스 복원 실패"
[[ "$cleanup_sql" == true ]] && rm -f "$sql_file"
return 1
fi
}
# 소스 코드 복원
restore_source_code() {
local backup_file="$1"
log "INFO" "소스 코드 복원 시작: $backup_file"
if [[ ! -f "$backup_file" ]]; then
log "ERROR" "백업 파일을 찾을 수 없음: $backup_file"
return 1
fi
# 기존 소스 코드 백업
local backup_existing="${PROJECT_ROOT}_backup_$(date +%Y%m%d_%H%M%S)"
log "INFO" "기존 소스 코드 백업: $backup_existing"
mv "$PROJECT_ROOT" "$backup_existing"
# 새로운 소스 코드 복원
mkdir -p "$PROJECT_ROOT"
if tar -xzf "$backup_file" -C "$PROJECT_ROOT" --strip-components=1; then
log "INFO" "✅ 소스 코드 복원 완료"
return 0
else
log "ERROR" "❌ 소스 코드 복원 실패"
# 실패 시 기존 코드 복구
rm -rf "$PROJECT_ROOT"
mv "$backup_existing" "$PROJECT_ROOT"
return 1
fi
}
# Docker 설정 복원
restore_docker_config() {
local backup_file="$1"
log "INFO" "Docker 설정 복원 시작: $backup_file"
if [[ ! -f "$backup_file" ]]; then
log "ERROR" "백업 파일을 찾을 수 없음: $backup_file"
return 1
fi
# 임시 디렉토리에 압축 해제
local temp_dir="/tmp/docker_restore_$(date +%s)"
mkdir -p "$temp_dir"
if tar -xzf "$backup_file" -C "$temp_dir"; then
# Docker Compose 파일 복원
local docker_dir=$(find "$temp_dir" -name "docker_*" -type d | head -1)
if [[ -d "$docker_dir" ]]; then
[[ -f "$docker_dir/docker-compose.yml" ]] && cp "$docker_dir/docker-compose.yml" "$PROJECT_ROOT/"
[[ -f "$docker_dir/.dev.env" ]] && cp "$docker_dir/.dev.env" "$PROJECT_ROOT/src/configs/env/"
log "INFO" "✅ Docker 설정 복원 완료"
rm -rf "$temp_dir"
return 0
else
log "ERROR" "❌ Docker 설정 파일을 찾을 수 없음"
rm -rf "$temp_dir"
return 1
fi
else
log "ERROR" "❌ Docker 설정 복원 실패"
rm -rf "$temp_dir"
return 1
fi
}
# 메인 복원 함수
main_restore() {
local restore_type="$1"
local component="$2"
local backup_file="$3"
log "INFO" "=== 백업 복원 시작 ($restore_type - $component) ==="
case "$restore_type" in
"full")
log "INFO" "전체 복원 시작..."
# 구현 필요
;;
"partial")
case "$component" in
"db"|"database")
restore_database "$backup_file"
;;
"source"|"code")
restore_source_code "$backup_file"
;;
"docker"|"config")
restore_docker_config "$backup_file"
;;
*)
log "ERROR" "지원하지 않는 컴포넌트: $component"
exit 1
;;
esac
;;
*)
log "ERROR" "지원하지 않는 복원 타입: $restore_type"
exit 1
;;
esac
}
# 사용법 출력
usage() {
echo "🔄 백업 복원 시스템"
echo ""
echo "사용법: $0 [명령어] [옵션...]"
echo ""
echo "명령어:"
echo " list - 백업 파일 목록 보기"
echo " info [backup_file] - 백업 파일 상세 정보"
echo " partial [component] [file] - 부분 복원"
echo " full [backup_date] - 전체 복원"
echo ""
echo "컴포넌트 (partial 시):"
echo " db, database - 데이터베이스만 복원"
echo " source, code - 소스 코드만 복원"
echo " docker, config - Docker 설정만 복원"
echo ""
echo "예시:"
echo " $0 list"
echo " $0 info /backup/data/database/db_backup.sql.gz"
echo " $0 partial db /backup/data/database/db_backup.sql.gz"
echo " $0 partial source /backup/data/daily/source_backup.tar.gz"
}
# 🆕 메인 실행 (list 명령어 포함)
case "${1:-help}" in
"list")
list_backups
;;
"info")
if [[ $# -ge 2 ]]; then
show_backup_details "$2"
else
echo "❌ 백업 파일 경로가 필요합니다"
usage
exit 1
fi
;;
"partial")
if [[ $# -ge 3 ]]; then
main_restore "$1" "$2" "$3"
else
echo "❌ 컴포넌트와 백업 파일이 필요합니다"
usage
exit 1
fi
;;
"full")
echo "🚧 전체 복원 기능은 구현 중입니다"
exit 1
;;
"help"|"-h"|"--help")
usage
;;
*)
echo "❌ 알 수 없는 명령어: ${1:-}"
usage
exit 1
;;
esac
EOF
restore.sh 는 신규 데이터를 이전 데이터가 덮어씌울 수 있는 위험이 있기 때문에, 자동으로 실행되지 않는다.
문제가 발생하였을 경우 수동으로 직접 실행하여 데이터를 복원한다.
9. 배포 후 백업 시스템 수동 테스트
9.1. 배포 상태 확인
# SSH로 서버 접속 후
cd /volume1/docker/NestJS_SNS
# 모든 컨테이너 상태 확인
sudo docker-compose ps
# 백업 컨테이너 상태 확인
sudo docker logs sns-dev-backup --tail 20
9.2. 백업 시스템 수동 테스트
9.2.1. 긴급 백업 테스트
# 긴급 백업 실행 (가장 빠른 테스트)
sudo docker exec sns-dev-backup /backup/scripts/backup.sh emergency
# 실행 결과 확인
sudo docker exec sns-dev-backup ls -la /backup/data/emergency/
# 백업 로그 실시간 확인
sudo docker logs sns-dev-backup -f
9.2.2. 일간 백업 테스트
# 일간 백업 실행
sudo docker exec sns-dev-backup /backup/scripts/backup.sh daily
# 백업 파일 확인
sudo docker exec sns-dev-backup ls -la /backup/data/daily/
# 데이터베이스 백업 확인
sudo docker exec sns-dev-backup ls -la /backup/data/database/
9.3. 정리 시스템 테스트
# 상태 리포트 생성
sudo docker exec sns-dev-backup /backup/scripts/cleanup.sh status
# 디스크 사용량 체크
sudo docker exec sns-dev-backup /backup/scripts/cleanup.sh check
# 전체 정리 실행
sudo docker exec sns-dev-backup /backup/scripts/cleanup.sh
9.4. 복원 시스템 테스트
9.4.1. 백업 파일 목록 확인
# 사용 가능한 백업 목록 보기
sudo docker exec sns-dev-backup /backup/scripts/restore.sh list
# 특정 타입 백업만 보기
sudo docker exec sns-dev-backup /backup/scripts/restore.sh list emergency
# 도움말
sudo docker exec sns-dev-backup /backup/scripts/restore.sh help
9.4.2. 부분 복원 테스트 (안전)
# 데이터베이스만 복원 (테스트용)
# 주의: 실제 DB가 덮어쓰여지므로 신중하게!
sudo docker exec sns-dev-backup /backup/scripts/restore.sh partial db /backup/data/database/[백업파일명]
9.5. 로그 모니터링 방법
9.5.1. 실시간 로그 확인
# 백업 컨테이너 로그 실시간 보기
sudo docker logs sns-dev-backup -f
# 마지막 50줄만 보기
sudo docker logs sns-dev-backup --tail 50
# 특정 시간대 로그 보기
sudo docker logs sns-dev-backup --since="2024-07-04T10:00:00" --until="2024-07-04T12:00:00"
9.5.2. 백업 전용 로그 파일 확인
# 컨테이너 내부 로그 파일 확인
sudo docker exec sns-dev-backup tail -f /backup/logs/backup.log
# 로그 파일 전체 보기
sudo docker exec sns-dev-backup cat /backup/logs/backup.log
# 에러 로그만 필터링
sudo docker exec sns-dev-backup grep "ERROR" /backup/logs/backup.log
# 최근 백업 로그만 보기
sudo docker exec sns-dev-backup grep "$(date +%Y-%m-%d)" /backup/logs/backup.log
9.6. 상세 테스트 시나리오
#!/bin/bash
# 백업 시스템 통합 테스트 스크립트
echo "🧪 백업 시스템 테스트 시작..."
# 1. 컨테이너 상태 확인
echo "1️⃣ 컨테이너 상태 확인"
sudo docker-compose ps | grep backup
# 2. 긴급 백업 테스트
echo "2️⃣ 긴급 백업 실행 중..."
sudo docker exec sns-dev-backup /backup/scripts/backup.sh emergency
# 3. 백업 파일 확인
echo "3️⃣ 백업 파일 확인"
sudo docker exec sns-dev-backup ls -la /backup/data/emergency/ | tail -3
# 4. 상태 리포트 생성
echo "4️⃣ 상태 리포트 생성"
sudo docker exec sns-dev-backup /backup/scripts/cleanup.sh status
# 5. 백업 목록 확인
echo "5️⃣ 백업 목록 확인"
sudo docker exec sns-dev-backup /backup/scripts/restore.sh list
# 6. 로그 확인
echo "6️⃣ 최근 로그 확인"
sudo docker logs sns-dev-backup --tail 10
echo "✅ 테스트 완료!"
'NestJS' 카테고리의 다른 글
Github Actions를 사용한 CI/CD - Slack 알림 (0) | 2025.07.03 |
---|---|
Github Actions를 사용한 CI/CD - 롤백, 모니터링, 로그 정리 (0) | 2025.07.02 |
Github Actions를 사용한 CI/CD 구축 (0) | 2025.06.30 |
시놀로지 나스 NestJS 개발서버 구축하기 - Docker Container (0) | 2025.06.19 |
[NestJS] winston 로그 구현하기 (1) | 2025.06.05 |