본문 바로가기
NestJS

Github Actions를 사용한 CI/CD - 백업 시스템 구축

by Programmer.Junny 2025. 7. 4.

각종 환경설정이나 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. 배포 후 백업 시스템 수동 테스트

빌드가 성공하면 sns-dev-backup 컨테이너가 생성된다.
로그 또한 정상적으로 호출된다.

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

sudo docker exec sns-dev-backup /backup/scripts/restore.sh list

9.4.2. 부분 복원 테스트 (안전)

# 데이터베이스만 복원 (테스트용)
# 주의: 실제 DB가 덮어쓰여지므로 신중하게!
sudo docker exec sns-dev-backup /backup/scripts/restore.sh partial db /backup/data/database/[백업파일명]

백업 후 DB 수정
백업을 복원
이전 백업으로 DB가 잘 적용되었다.

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 "✅ 테스트 완료!"

최근댓글

최근글

skin by © 2024 ttuttak