

TL;DR
- cron과 Bash 스크립트로 서버 운영을 자동화한다.
- 디스크 용량 모니터링, 로그 정리, DB 백업을 자동화해서 수동 작업을 최소화하고 장애를 예방한다.
📚 클라우드 인프라 구축기 시리즈
1. KakaoCloud 2-Tier 아키텍처 설계
2. GitHub Actions로 CI/CD 구축하기
3. SSH 브루트포스 5,227건 대응기
4. Nginx 설정으로 SSL Labs A+ 달성하기
5. 서버 자동화 스크립트 ⬅️ 현재 글
서버를 운영하다 보면 반복적인 작업이 생긴다.
이런 작업을 매번 수동으로 하면 시간도 들고 잊어버리기 쉽다.
cron과 Bash 스크립트로 자동화하면 이런 문제를 해결할 수 있다.
| 작업 | 목적 | 주기 |
|---|---|---|
| 디스크 모니터링 | 용량 부족 사전 감지 | 매일 |
| 로그 정리 | 불필요한 로그 삭제 | 매주 |
| 헬스체크 | 서비스 상태 확인 | 매일 |
| DB 백업 | 데이터 손실 방지 | 매일 |
| Docker 정리 | 미사용 이미지/캐시 삭제 | 매주 |

~/scripts/
├── disk-alert.sh # 디스크 용량 알림
├── weekly-cleanup.sh # 주간 정리 (로그 + Docker)
├── healthcheck.sh # 서비스 헬스체크
└── backup-db.sh # DB 백업 (/usr/local/bin/)
서버 디스크가 가득 차면 서비스가 중단된다.
Docker 로그, 시스템 로그가 쌓이면서 용량이 부족해지는 경우가 많다.

~/scripts/disk-alert.sh
#!/bin/bash
# 설정
THRESHOLD=80 # 경고 기준 (%)
LOG_FILE="/var/log/disk-alert.log"
# 디스크 사용량 확인
USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
# 로그 기록
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Disk usage: ${USAGE}%" >> $LOG_FILE
# 임계치 초과 시 경고
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: Disk usage exceeded ${THRESHOLD}%" >> $LOG_FILE
# 용량 많이 차지하는 디렉토리 확인
echo "Top 5 directories:" >> $LOG_FILE
du -h --max-depth=1 / 2>/dev/null | sort -hr | head -5 >> $LOG_FILE
fi
| 명령어 | 설명 |
|---|---|
df / | 루트 파티션 용량 확인 |
awk 'NR==2 {print $5}' | 두 번째 줄의 5번째 컬럼 (사용률) 추출 |
sed 's/%//' | % 기호 제거 |
du -h --max-depth=1 | 1단계 깊이까지 디렉토리 용량 확인 |
chmod +x ~/scripts/disk-alert.sh

| 대상 | 명령어 | 효과 |
|---|---|---|
| 시스템 로그 | journalctl --vacuum-time=7d | 7일 이전 로그 삭제 |
| Docker 캐시 | docker system prune -f | 미사용 컨테이너/네트워크 삭제 |
| Docker 빌더 캐시 | docker builder prune -f | 빌드 캐시 삭제 |
| 임시 파일 | rm -rf /tmp/* | 임시 파일 삭제 |

~/scripts/weekly-cleanup.sh
#!/bin/bash
LOG_FILE="/var/log/weekly-cleanup.log"
echo "====== Weekly Cleanup Started: $(date) ======" >> $LOG_FILE
# 1. 시스템 로그 정리 (7일 이전)
echo "[$(date '+%H:%M:%S')] Cleaning system logs..." >> $LOG_FILE
sudo journalctl --vacuum-time=7d >> $LOG_FILE 2>&1
# 2. Docker 정리
echo "[$(date '+%H:%M:%S')] Cleaning Docker..." >> $LOG_FILE
docker system prune -f >> $LOG_FILE 2>&1
docker builder prune -f >> $LOG_FILE 2>&1
# 3. 임시 파일 정리
echo "[$(date '+%H:%M:%S')] Cleaning temp files..." >> $LOG_FILE
sudo rm -rf /tmp/* 2>/dev/null
# 4. 정리 후 디스크 용량 확인
echo "[$(date '+%H:%M:%S')] Disk usage after cleanup:" >> $LOG_FILE
df -h / >> $LOG_FILE
echo "====== Weekly Cleanup Completed: $(date) ======" >> $LOG_FILE

$ tail -20 /var/log/weekly-cleanup.log
freed 0B는 "정리할 게 없다"는 뜻이라 오히려 자동화가 잘 되고 있다는 증거이다.
매주 금요일에 돌면서 쌓이기 전에 미리 정리하고 있고 잘 동작하고 있다.
프로젝트 운영 중 실제로 정리한 결과:
$ sudo journalctl --vacuum-time=7d
Vacuuming done, freed 411.5M of archived journals from /var/log/journal
| 대상 | 정리 전 | 정리 후 | 절약 |
|---|---|---|---|
| 시스템 로그 | 427.5MB | 16MB | 411.5MB |
| Docker 캐시 | ~2GB | ~500MB | ~1.5GB |
서비스가 정상 작동하는지 매일 확인한다. Backend, Nginx, 디스크, 메모리 상태를 한눈에 파악할 수 있다.
| 항목 | 확인 방법 | 정상 기준 |
|---|---|---|
| Backend | HTTP 상태코드 | 200 |
| Nginx | systemctl 상태 | active |
| Disk | df 사용률 | 80% 미만 |
| Memory | free 사용량 | - |

~/scripts/healthcheck.sh
#!/bin/bash
LOG_FILE=~/logs/health-$(date +%Y%m%d).log
mkdir -p ~/logs
echo "=== Health Check: $(date) ===" >> $LOG_FILE
# Backend 체크
BACKEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/)
if [ "$BACKEND" = "200" ]; then
echo "✅ Backend: OK" >> $LOG_FILE
else
echo "❌ Backend: FAIL ($BACKEND)" >> $LOG_FILE
fi
# Nginx 체크
NGINX=$(sudo systemctl is-active nginx)
echo "📦 Nginx: $NGINX" >> $LOG_FILE
# 디스크 사용량
DISK=$(df / | tail -1 | awk '{print $5}')
echo "💾 Disk: $DISK" >> $LOG_FILE
# 메모리
MEM=$(free -h | grep Mem | awk '{print $3"/"$2}')
echo "🧠 Memory: $MEM" >> $LOG_FILE
| 명령어 | 설명 |
|---|---|
curl -s -o /dev/null -w "%{http_code}" | HTTP 상태코드만 출력 |
systemctl is-active | 서비스 상태 확인 (active/inactive) |
df / \| tail -1 \| awk '{print $5}' | 디스크 사용률 추출 |
free -h \| grep Mem | 메모리 사용량 확인 |

$ ~/scripts/healthcheck.sh && cat ~/logs/health-$(date +%Y%m%d).log
=== Health Check: Mon Feb 2 09:00:00 UTC 2026 ===
✅ Backend: OK
📦 Nginx: active
💾 Disk: 19%
🧠 Memory: 456Mi/15Gi
| 항목 | 설정 |
|---|---|
| 백업 주기 | 평일 오전 8시 |
| 보관 기간 | 7일 |
| 백업 위치 | /var/backups/postgresql |
| 백업 형식 | SQL 덤프 (gzip 압축) |
스크립트에 비밀번호를 직접 넣는 것은 보안상 좋지 않다.
별도 환경변수 파일을 사용한다.
/etc/backup-db.env

# root만 읽을 수 있도록 설정
sudo touch /etc/backup-db.env
sudo chmod 600 /etc/backup-db.env
# 내용 작성
sudo vi /etc/backup-db.env
DB_PASSWORD="your-password-here"
/usr/local/bin/backup-db.sh

#!/bin/bash
# 환경변수 로드 (비밀번호 분리)
source /etc/backup-db.env
# 설정
BACKUP_DIR="/var/backups/postgresql"
DB_NAME="moviesir"
DB_USER="movigation"
RETENTION_DAYS=7
# 백업 디렉토리 생성
mkdir -p $BACKUP_DIR
# 백업 파일명 (날짜 포함)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz"
# PostgreSQL 덤프 + gzip 압축
echo "[$(date)] Starting backup..."
PGPASSWORD=$DB_PASSWORD pg_dump -h localhost -U $DB_USER $DB_NAME | gzip > $BACKUP_FILE
# 백업 성공 확인
if [ $? -eq 0 ]; then
echo "[$(date)] Backup completed: $BACKUP_FILE"
echo "[$(date)] File size: $(du -h $BACKUP_FILE | cut -f1)"
else
echo "[$(date)] Backup FAILED!"
exit 1
fi
# 오래된 백업 삭제 (7일 이전)
echo "[$(date)] Removing old backups..."
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
# 남은 백업 파일 목록
echo "[$(date)] Current backups:"
ls -lh $BACKUP_DIR/*.sql.gz 2>/dev/null
| 항목 | 설명 |
|---|---|
source /etc/backup-db.env | 비밀번호를 별도 파일에서 로드 (보안) |
/var/backups/postgresql | 시스템 표준 백업 경로 사용 |
gzip | 압축으로 용량 ~75% 절약 |
$? | 이전 명령어 종료 코드 확인 (에러 처리) |

$ ls /var/backups/postgresql/
분 시 일 월 요일 명령어
* * * * * command
| 필드 | 범위 | 설명 |
|---|---|---|
| 분 | 0-59 | 실행 시각 (분) |
| 시 | 0-23 | 실행 시각 (시) |
| 일 | 1-31 | 실행 날짜 |
| 월 | 1-12 | 실행 월 |
| 요일 | 0-7 | 0,7=일요일, 1=월요일 |


# crontab 편집 (root 권한 필요 시)
sudo crontab -e
# 아래 내용 추가
# 평일 오전 8시: DB 백업 (GPU 서버)
0 8 * * 1-5 /usr/local/bin/backup-db.sh >> /var/log/db-backup.log 2>&1
# 평일 오전 9:30: 헬스체크 (App 서버)
30 9 * * 1-5 /home/ubuntu/scripts/healthcheck.sh
# 평일 오후 5:30: 디스크 용량 체크 (App 서버)
30 17 * * 1-5 /home/ubuntu/scripts/disk-alert.sh
# 매주 금요일 오후 5시: 주간 정리 (App 서버)
0 17 * * 5 /home/ubuntu/scripts/weekly-cleanup.sh
💡 시간 설정 참고
일반적으로 DB 백업은 새벽 3시(트래픽 최저 시간)에 실행하는 것이 업계 표준이다. 하지만 이 프로젝트의 서버는 09:00~18:00에만 운영되므로,
서버 시작 직후인 오전 8시에 백업을 실행하도록 설정했다.
운영 환경에 맞게 조정하면 된다.

# 현재 crontab 확인
sudo crontab -l
# cron 서비스 상태 확인
sudo systemctl status cron

# cron 실행 로그
grep CRON /var/log/syslog | tail -20
문제: 백업 파일이 0바이트로 생성됨
$ ls -lh ~/backup/
-rw-r--r-- 1 ubuntu ubuntu 0 Jan 19 03:00 moviesir_20260119_030000.sql.gz
원인: DB 비밀번호 변경 후 스크립트 미업데이트
해결:
# 스크립트의 비밀번호 업데이트
sudo vi /usr/local/bin/backup-db.sh
# DB_PASSWORD 값 수정
# 수동 실행으로 테스트
/usr/local/bin/backup-db.sh
교훈: DB 비밀번호 변경 시 백업 스크립트도 함께 업데이트해야 한다.
문제: crontab에 등록했는데 스크립트가 실행되지 않음
체크리스트:
| 확인 사항 | 명령어 |
|---|---|
| 실행 권한 | ls -l ~/scripts/*.sh |
| cron 서비스 상태 | sudo systemctl status cron |
| 스크립트 경로 | 절대 경로 사용 여부 |
| 환경변수 | cron은 최소 환경변수만 가짐 |
해결:
# 1. 실행 권한 부여
chmod +x ~/scripts/*.sh
# 2. 절대 경로 사용
# Bad: ./scripts/disk-alert.sh
# Good: /home/ubuntu/scripts/disk-alert.sh
# 3. 환경변수 명시 (필요 시)
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * /usr/local/bin/backup-db.sh
문제: journalctl --vacuum-time 실행 시 권한 오류
$ journalctl --vacuum-time=7d
Failed to rotate journal: Permission denied
해결:
# sudo 사용
sudo journalctl --vacuum-time=7d
| 원칙 | 설명 |
|---|---|
| 로그 남기기 | 언제 무엇을 했는지 기록 |
| 에러 처리 | 실패 시 알 수 있도록 |
| 멱등성 | 여러 번 실행해도 같은 결과 |
| 주석 작성 | 나중에 봐도 이해할 수 있게 |
| 항목 | 확인 주기 | 방법 |
|---|---|---|
| 백업 성공 여부 | 매일 | 로그 파일 확인 |
| 디스크 용량 | 매주 | df -h |
| cron 실행 여부 | 매주 | grep CRON /var/log/syslog |
Slack이나 Discord로 알림을 보내면 더 편하다:
# Slack Webhook 예시
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"[서버] 디스크 용량 80% 초과!"}' \
$SLACK_WEBHOOK_URL
| 항목 | Before | After |
|---|---|---|
| 백업 | 수동 (가끔 잊음) | 매일 자동 |
| 로그 정리 | 디스크 가득 찬 후 정리 | 매주 자동 |
| 디스크 모니터링 | 문제 발생 후 인지 | 사전 감지 |
| 항목 | 효과 |
|---|---|
| 시간 | 주 1시간 → 0시간 (자동화) |
| 디스크 | 매주 ~2GB 정리 |
| 안정성 | 디스크 풀 장애 예방 |
서버 자동화의 핵심은 반복 작업을 스크립트로 만들고 cron으로 예약하는 것이다.
| 단계 | 내용 |
|---|---|
| 1 | 반복 작업 파악 |
| 2 | Bash 스크립트 작성 |
| 3 | 수동 실행으로 테스트 |
| 4 | cron에 등록 |
| 5 | 로그로 실행 확인 |
작은 스크립트부터 시작해서 점점 자동화 범위를 늘려가면 된다.
#!/bin/bash
THRESHOLD=80
LOG_FILE="/var/log/disk-alert.log"
USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Disk usage: ${USAGE}%" >> $LOG_FILE
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: Disk usage exceeded ${THRESHOLD}%" >> $LOG_FILE
du -h --max-depth=1 / 2>/dev/null | sort -hr | head -5 >> $LOG_FILE
fi
#!/bin/bash
LOG_FILE="/var/log/weekly-cleanup.log"
echo "====== Weekly Cleanup: $(date) ======" >> $LOG_FILE
sudo journalctl --vacuum-time=7d >> $LOG_FILE 2>&1
docker system prune -f >> $LOG_FILE 2>&1
docker builder prune -f >> $LOG_FILE 2>&1
sudo rm -rf /tmp/* 2>/dev/null
df -h / >> $LOG_FILE
echo "====== Completed ======" >> $LOG_FILE
#!/bin/bash
LOG_FILE=~/logs/health-$(date +%Y%m%d).log
mkdir -p ~/logs
echo "=== Health Check: $(date) ===" >> $LOG_FILE
BACKEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/)
if [ "$BACKEND" = "200" ]; then
echo "✅ Backend: OK" >> $LOG_FILE
else
echo "❌ Backend: FAIL ($BACKEND)" >> $LOG_FILE
fi
NGINX=$(sudo systemctl is-active nginx)
echo "📦 Nginx: $NGINX" >> $LOG_FILE
DISK=$(df / | tail -1 | awk '{print $5}')
echo "💾 Disk: $DISK" >> $LOG_FILE
MEM=$(free -h | grep Mem | awk '{print $3"/"$2}')
echo "🧠 Memory: $MEM" >> $LOG_FILE
#!/bin/bash
source /etc/backup-db.env
BACKUP_DIR="/var/backups/postgresql"
DB_NAME="moviesir"
DB_USER="movigation"
RETENTION_DAYS=7
mkdir -p $BACKUP_DIR
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz"
PGPASSWORD=$DB_PASSWORD pg_dump -h localhost -U $DB_USER $DB_NAME | gzip > $BACKUP_FILE
if [ $? -eq 0 ]; then
echo "[$(date)] Backup completed: $(du -h $BACKUP_FILE | cut -f1)"
else
echo "[$(date)] Backup FAILED!"
exit 1
fi
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
# GPU 서버 (sudo crontab -e)
0 8 * * 1-5 /usr/local/bin/backup-db.sh >> /var/log/db-backup.log 2>&1
# App 서버 (sudo crontab -e)
30 9 * * 1-5 /home/ubuntu/scripts/healthcheck.sh
30 17 * * 1-5 /home/ubuntu/scripts/disk-alert.sh
0 17 * * 5 /home/ubuntu/scripts/weekly-cleanup.sh
이 글은 스나이퍼팩토리 카카오클라우드 AIaaS 마스터 클래스 2기 프로젝트 경험을 바탕으로 작성되었습니다.