Ghost 블로그 백업 자동화 완벽 가이드

Ghost 블로그의 MySQL, 콘텐츠를 cron과 GitHub으로 자동 백업하는 시스템 구축 가이드입니다.

1. 왜 백업이 중요한가

셀프 호스팅에서 백업은 선택이 아닌 필수입니다. 다음 상황에서 데이터를 보호합니다:

  • 서버 장애: 클라우드도 완벽하지 않음
  • 해킹/랜섬웨어: 복원 가능한 백업이 최후의 보루
  • 실수: 게시물 삭제, 잘못된 설정 변경
  • 업그레이드 실패: 롤백 필요 시

Ghost(Pro)는 자동 백업이 포함되지만, 셀프 호스팅은 직접 구축해야 합니다.


2. 백업 전략 설계

2.1 백업 대상

대상 중요도 방법
MySQL 데이터베이스 ⭐⭐⭐ mysqldump
Ghost 콘텐츠 (이미지, 테마) ⭐⭐⭐ tar.gz
docker-compose.yml ⭐⭐ 파일 복사

2.2 3-2-1 백업 원칙

3-2-1 백업 원칙:
├── 3개의 복사본
├── 2개의 다른 저장 매체
└── 1개는 오프사이트 (서버 외부)

이 가이드의 구성:

저장소 위치 보관 기간
서버 로컬 ~/backup/ 7일
GitHub Private Repo 클라우드 무제한 (최신 1개)

2.3 왜 GitHub인가?

저장소 무료 용량 장점 단점
GitHub Private 무제한 무료, 버전 관리, 간편 단일 파일 100MB 제한
Cloudflare R2 10GB S3 호환 설정 복잡
Google Drive 15GB 용량 큼 API 설정 필요
Backblaze B2 10GB 저렴 유료 전환 시 비용

Ghost 블로그의 백업 파일은 보통 수십 MB 수준이므로 GitHub이 가장 간편합니다.


3. 백업 스크립트 작성

3.1 스크립트 디렉토리 생성

mkdir -p ~/scripts ~/backup

3.2 backup.sh 작성

cat > ~/scripts/backup.sh << 'EOF'
#!/bin/bash
# Ghost 블로그 백업 스크립트 (GitHub 연동)

set -e

# === 설정 ===
BACKUP_DIR="$HOME/backup"
GITHUB_REPO="$HOME/backup-repo"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7

# MySQL 정보 (docker-compose.yml과 동일하게 설정)
MYSQL_USER="ghost"
MYSQL_PASSWORD="YOUR_GHOST_DB_PASSWORD"  # ← 실제 비밀번호로 변경
MYSQL_DATABASE="ghost"

# 색상 출력
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

echo -e "${GREEN}=== Ghost 백업 시작 [$(date)] ===${NC}"

# 1. MySQL 데이터베이스 백업
echo -e "${YELLOW}[1/4] MySQL 데이터베이스 백업 중...${NC}"
docker exec ghost-db mysqldump -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE 2>/dev/null > "$BACKUP_DIR/ghost-db-$DATE.sql"
echo "  → ghost-db-$DATE.sql 생성 완료"

# 2. Ghost 콘텐츠 볼륨 백업
echo -e "${YELLOW}[2/4] Ghost 콘텐츠 백업 중...${NC}"
docker run --rm \
  -v ghost_ghost-content:/data:ro \
  -v "$BACKUP_DIR":/backup \
  alpine tar czf "/backup/ghost-content-$DATE.tar.gz" -C /data .
echo "  → ghost-content-$DATE.tar.gz 생성 완료"

# 3. Docker Compose 설정 복사
echo -e "${YELLOW}[3/4] Docker 설정 백업 중...${NC}"
cp ~/docker/ghost/docker-compose.yml "$BACKUP_DIR/docker-compose-$DATE.yml"
echo "  → docker-compose-$DATE.yml 생성 완료"

# 4. GitHub에 Push
echo -e "${YELLOW}[4/4] GitHub에 백업 업로드 중...${NC}"
if [ -d "$GITHUB_REPO/.git" ]; then
    cd "$GITHUB_REPO"

    # 기존 파일 삭제 (최신 백업만 유지)
    rm -f "$GITHUB_REPO"/ghost-db-*.sql
    rm -f "$GITHUB_REPO"/ghost-content-*.tar.gz
    rm -f "$GITHUB_REPO"/docker-compose-*.yml

    # 새 백업 복사
    cp "$BACKUP_DIR/ghost-db-$DATE.sql" "$GITHUB_REPO/"
    cp "$BACKUP_DIR/ghost-content-$DATE.tar.gz" "$GITHUB_REPO/"
    cp "$BACKUP_DIR/docker-compose-$DATE.yml" "$GITHUB_REPO/"

    # Git commit & push
    git add -A
    git commit -m "Backup: $DATE" || echo "No changes to commit"
    git push origin main
    echo "  → GitHub push 완료"
else
    echo -e "${RED}  → GitHub repo 미설정, 로컬 백업만 진행${NC}"
fi

# 5. 오래된 로컬 백업 삭제
echo -e "${YELLOW}[정리] ${RETENTION_DAYS}일 이상 된 로컬 백업 삭제 중...${NC}"
find "$BACKUP_DIR" -name "ghost-*" -mtime +$RETENTION_DAYS -delete 2>/dev/null || true
find "$BACKUP_DIR" -name "docker-compose-*" -mtime +$RETENTION_DAYS -delete 2>/dev/null || true

# 6. 결과 출력
echo -e "${GREEN}=== 백업 완료 ===${NC}"
echo "로컬 백업: $BACKUP_DIR"
ls -lh "$BACKUP_DIR" | grep -E "(ghost-|docker-compose-).*$DATE" || true
EOF

3.3 실행 권한 부여

chmod +x ~/scripts/backup.sh

3.4 테스트 실행

~/scripts/backup.sh

정상 출력:

=== Ghost 백업 시작 [Sun Jan 18 22:45:45 KST 2026] ===
[1/4] MySQL 데이터베이스 백업 중...
  → ghost-db-20260118_224545.sql 생성 완료
[2/4] Ghost 콘텐츠 백업 중...
  → ghost-content-20260118_224545.tar.gz 생성 완료
[3/4] Docker 설정 백업 중...
  → docker-compose-20260118_224545.yml 생성 완료
[4/4] GitHub에 백업 업로드 중...
  → GitHub repo 미설정, 로컬 백업만 진행
[정리] 7일 이상 된 로컬 백업 삭제 중...
=== 백업 완료 ===

4. GitHub 백업 저장소 설정

4.1 GitHub Private Repo 생성

  1. GitHub 접속
  2. New repository 클릭
  3. 설정:
Repository name: ghost-backup
Visibility: Private  # ← 중요!
Initialize: (체크 안 함)
  1. Create repository 클릭

4.2 서버에서 SSH 키 생성 (없는 경우)

# SSH 키 생성
ssh-keygen -t ed25519 -C "server-backup"

# 공개키 확인
cat ~/.ssh/id_ed25519.pub

4.3 GitHub에 SSH 키 등록

방법 1: Deploy Key (권장)

  1. GitHub repo → SettingsDeploy keys
  2. Add deploy key
  3. Title: backup-server
  4. Key: 공개키 붙여넣기
  5. Allow write access
  6. Add key

방법 2: 계정 SSH Key

  1. GitHub → SettingsSSH and GPG keys
  2. New SSH key
  3. Key 붙여넣기

4.4 서버에서 Git 설정

# Git 사용자 설정
git config --global user.name "backup-bot"
git config --global user.email "[email protected]"

# SSH 연결 테스트
ssh -T [email protected]
# Hi username! You've successfully authenticated...

4.5 Repo 클론

git clone [email protected]:YOUR_USERNAME/ghost-backup.git ~/backup-repo

4.6 백업 스크립트 재실행

~/scripts/backup.sh

이제 GitHub에 백업이 push됩니다:

[4/4] GitHub에 백업 업로드 중...
[main abc1234] Backup: 20260118_224545
 3 files changed, 2616 insertions(+)
  → GitHub push 완료

5. Cron 자동화 설정

5.1 서버 시간대 확인/설정

# 현재 시간대 확인
timedatectl

# 한국 시간대로 변경 (선택)
sudo timedatectl set-timezone Asia/Seoul

5.2 Cron Job 등록

# crontab 편집
crontab -e

다음 줄 추가:

# Ghost 백업 - 매일 새벽 1시 (KST)
0 1 * * * /home/ubuntu/scripts/backup.sh >> /home/ubuntu/backup/backup.log 2>&1

Cron 표현식 설명:

0 1 * * *
│ │ │ │ │
│ │ │ │ └── 요일 (0-7, 0과 7은 일요일)
│ │ │ └──── 월 (1-12)
│ │ └────── 일 (1-31)
│ └──────── 시 (0-23)
└────────── 분 (0-59)

5.3 Cron 설정 확인

crontab -l

출력:

0 1 * * * /home/ubuntu/scripts/backup.sh >> /home/ubuntu/backup/backup.log 2>&1

5.4 로그 확인

백업 실행 후 로그 확인:

tail -50 ~/backup/backup.log

6. 백업 복원 방법

6.1 MySQL 복원

# 백업 파일로 복원
docker exec -i ghost-db mysql -u ghost -pYOUR_PASSWORD ghost < ~/backup/ghost-db-20260118_224545.sql

6.2 Ghost 콘텐츠 복원

# 기존 볼륨 삭제 (주의!)
docker compose down
docker volume rm ghost_ghost-content

# 새 볼륨에 복원
docker volume create ghost_ghost-content
docker run --rm \
  -v ghost_ghost-content:/data \
  -v ~/backup:/backup \
  alpine tar xzf /backup/ghost-content-20260118_224545.tar.gz -C /data

# 컨테이너 재시작
docker compose up -d

6.3 전체 복원 스크립트

cat > ~/scripts/restore.sh << 'EOF'
#!/bin/bash
# Ghost 복원 스크립트

set -e

BACKUP_DATE=$1

if [ -z "$BACKUP_DATE" ]; then
    echo "Usage: $0 <BACKUP_DATE>"
    echo "Example: $0 20260118_224545"
    exit 1
fi

BACKUP_DIR="$HOME/backup"
DB_FILE="$BACKUP_DIR/ghost-db-$BACKUP_DATE.sql"
CONTENT_FILE="$BACKUP_DIR/ghost-content-$BACKUP_DATE.tar.gz"

# 파일 존재 확인
if [ ! -f "$DB_FILE" ] || [ ! -f "$CONTENT_FILE" ]; then
    echo "Error: Backup files not found for $BACKUP_DATE"
    exit 1
fi

echo "=== Ghost 복원 시작 ==="

# 1. 컨테이너 중지
cd ~/docker/ghost
docker compose down

# 2. MySQL 복원
echo "[1/3] MySQL 복원 중..."
docker compose up -d ghost-db
sleep 10  # DB 시작 대기
docker exec -i ghost-db mysql -u ghost -pYOUR_PASSWORD ghost < "$DB_FILE"

# 3. 콘텐츠 복원
echo "[2/3] 콘텐츠 복원 중..."
docker volume rm ghost_ghost-content 2>/dev/null || true
docker volume create ghost_ghost-content
docker run --rm \
  -v ghost_ghost-content:/data \
  -v "$BACKUP_DIR":/backup \
  alpine tar xzf "/backup/ghost-content-$BACKUP_DATE.tar.gz" -C /data

# 4. 컨테이너 시작
echo "[3/3] 컨테이너 시작 중..."
docker compose up -d

echo "=== 복원 완료 ==="
EOF

chmod +x ~/scripts/restore.sh

사용법:

~/scripts/restore.sh 20260118_224545

7. 모니터링 및 알림 (선택)

7.1 백업 성공 여부 확인 스크립트

cat > ~/scripts/check-backup.sh << 'EOF'
#!/bin/bash
# 백업 상태 확인

TODAY=$(date +%Y%m%d)
BACKUP_DIR="$HOME/backup"

# 오늘 백업 파일 확인
DB_FILE=$(ls -1 "$BACKUP_DIR"/ghost-db-${TODAY}*.sql 2>/dev/null | tail -1)
CONTENT_FILE=$(ls -1 "$BACKUP_DIR"/ghost-content-${TODAY}*.tar.gz 2>/dev/null | tail -1)

if [ -n "$DB_FILE" ] && [ -n "$CONTENT_FILE" ]; then
    echo "✅ 오늘 백업 성공"
    echo "  DB: $(basename $DB_FILE) ($(du -h $DB_FILE | cut -f1))"
    echo "  Content: $(basename $CONTENT_FILE) ($(du -h $CONTENT_FILE | cut -f1))"
else
    echo "❌ 오늘 백업 없음!"
    exit 1
fi
EOF

chmod +x ~/scripts/check-backup.sh

7.2 Discord/Slack 알림 (선택)

# backup.sh 끝에 추가
WEBHOOK_URL="https://discord.com/api/webhooks/xxx/yyy"
curl -H "Content-Type: application/json" \
  -d "{\"content\": \"✅ Ghost 백업 완료: $DATE\"}" \
  "$WEBHOOK_URL"

8. 트러블슈팅

8.1 mysqldump 권한 오류

에러:

mysqldump: Error: 'Access denied; you need the PROCESS privilege'

해결: 이 경고는 무시해도 됩니다. 실제 데이터는 정상 백업됩니다.

8.2 Docker 볼륨 이름 확인

docker volume ls | grep ghost

볼륨 이름이 다르면 스크립트 수정 필요:

# 예: ghost-ghost-content 대신 myproject_ghost-content인 경우
-v myproject_ghost-content:/data:ro

8.3 GitHub Push 실패

# SSH 연결 테스트
ssh -T [email protected]

# 권한 확인 (Deploy Key에 Write 권한 있는지)
# GitHub repo → Settings → Deploy keys → Allow write access ✅

8.4 Cron이 실행 안 됨

# Cron 서비스 상태 확인
sudo systemctl status cron

# Cron 로그 확인
grep CRON /var/log/syslog | tail -20

9. 핵심 개념 정리

개념 설명
Docker 볼륨 컨테이너 데이터 영구 저장소
mysqldump MySQL 데이터베이스 백업 도구
Cron Linux 작업 스케줄러
Deploy Key 특정 repo만 접근 가능한 SSH 키
3-2-1 백업 3개 복사본, 2개 매체, 1개 오프사이트

10. 베스트 프랙티스

체크리스트

  • [ ] 백업 스크립트 테스트 실행
  • [ ] GitHub repo Private 설정
  • [ ] Deploy Key에 Write 권한 부여
  • [ ] Cron 로그 정기 확인
  • [ ] 복원 테스트 (연 1회 이상)

권장 백업 주기

상황 주기
활발한 블로그 매일
가끔 포스팅 주 1회
거의 업데이트 없음 월 1회

11. FAQ

Q: 백업 파일 크기는 얼마나 되나요?
A: 콘텐츠 양에 따라 다르지만, 일반적인 블로그는 DB 수백 KB ~ 수 MB, 콘텐츠(이미지 포함) 수십 MB ~ 수백 MB입니다.

Q: GitHub 100MB 파일 제한에 걸리면?
A: Git LFS를 사용하거나, 이미지를 Cloudflare R2 같은 외부 스토리지로 분리하세요.

Q: 서버가 죽으면 복원할 수 있나요?
A: GitHub에 백업이 있다면 새 서버에서 Docker 설치 후 복원 가능합니다.

Q: 로컬 백업만으로 충분한가요?
A: 서버 자체에 문제가 생기면 로컬 백업도 함께 사라집니다. 오프사이트 백업(GitHub)을 권장합니다.


12. 다음 단계

백업 자동화까지 완료했습니다! 다음으로 검색엔진에 블로그를 등록합니다.

시리즈 목차:

  1. Oracle Cloud 무료 서버 세팅
  2. Ghost 블로그 Docker 설치
  3. Ghost 블로그 백업 자동화 ← 현재 글
  4. 검색엔진 등록 (Google/Naver)
  5. Ghost 6.0 업그레이드
  6. Ghost ActivityPub 설정 (Fediverse)
  7. Ghost Analytics 설정 (Tinybird)

13. 참고 자료