Docker Compose 환경에서 Ghost 블로그를 정기 백업하는 스크립트
Docker Compose 기반 Ghost 백업 스크립트입니다. JSON export만이 아니라 DB dump, content 압축, Compose 설정, 보관 주기, 복구 테스트까지 운영 기준으로 정리했습니다.
Docker Compose로 운영하는 Ghost 블로그를 백업할 때 ghost export만 저장하면 충분하지 않습니다. Ghost Admin의 JSON export는 글, 페이지, 태그, 설정 같은 콘텐츠 이동에는 유용하지만, 운영 복구 관점에서는 데이터베이스 덤프, 업로드 이미지, 테마, Compose 설정, 환경 파일까지 함께 보관해야 합니다.
백업 대상 구분
Ghost 복구에 필요한 항목은 보통 다음 네 가지입니다.
- 데이터베이스: 게시물, 사용자, 설정, 멤버 데이터가 들어갑니다.
- content 디렉터리: 업로드 이미지, 테마, redirects, routes 파일이 들어갑니다.
- Compose 파일: 서비스 이름, 볼륨, 네트워크, 이미지 태그를 재현하기 위해 필요합니다.
- 환경 설정: SMTP, URL, DB 접속 정보 같은 런타임 설정입니다. 실제 비밀값은 노출되지 않도록 권한을 제한합니다.
아래 예시는 Compose 서비스 이름이 ghost, db이고, Ghost content가 Docker volume 또는 bind mount로 /var/lib/ghost/content에 연결되어 있다는 전제입니다. 실제 서비스 이름은 먼저 확인하세요.
docker compose ps
docker compose config --services
docker compose config --volumes
백업 스크립트 예시
이 스크립트는 DB dump, content 압축, Compose 설정 복사, 보관 기간 정리를 한 번에 처리합니다. 비밀번호를 스크립트에 직접 적지 않고, Compose 환경 변수나 root 전용 .env에서 읽는 구조로 둡니다.
#!/usr/bin/env bash
set -Eeuo pipefail
APP_DIR="/opt/ghost"
BACKUP_ROOT="/srv/backups/ghost"
DATE="$(date +%Y%m%d_%H%M%S)"
DEST="${BACKUP_ROOT}/${DATE}"
RETENTION_DAYS=14
mkdir -p "${DEST}"
chmod 700 "${BACKUP_ROOT}" "${DEST}"
cd "${APP_DIR}"
echo "[1/4] compose metadata"
docker compose ps > "${DEST}/compose-ps.txt"
docker compose config > "${DEST}/compose-config.rendered.yml"
cp -a docker-compose.yml "${DEST}/docker-compose.yml"
[ -f .env ] && cp -a .env "${DEST}/env.redacted-review-required"
echo "[2/4] database dump"
docker compose exec -T db sh -c 'mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' \
| gzip -9 > "${DEST}/ghost-mysql.sql.gz"
echo "[3/4] ghost content archive"
docker compose exec -T ghost tar -C /var/lib/ghost -czf - content \
> "${DEST}/ghost-content.tar.gz"
echo "[4/4] optional Ghost Admin export"
docker compose exec -T ghost sh -lc 'cd /var/lib/ghost && ghost export --no-prompt' \
> "${DEST}/ghost-admin-export.log" 2>&1 || true
sha256sum "${DEST}"/* > "${DEST}/SHA256SUMS"
find "${BACKUP_ROOT}" -mindepth 1 -maxdepth 1 -type d -mtime +"${RETENTION_DAYS}" -print -exec rm -rf {} \;
echo "backup completed: ${DEST}"
docker compose exec -T를 쓰는 이유는 크론처럼 TTY가 없는 환경에서도 안정적으로 실행하기 위해서입니다. DB 서비스가 MySQL이 아니라 MariaDB라면 mysqldump가 그대로 있거나 mariadb-dump를 써야 할 수 있습니다.
권한과 보관 위치
백업에는 사용자 정보와 환경 설정이 섞일 수 있으므로 일반 웹 루트 아래에 두면 안 됩니다. 서버 로컬에 임시 보관한 뒤 다른 디스크, NAS, 오브젝트 스토리지 등 별도 장애 도메인으로 복제하는 것이 좋습니다.
sudo install -d -m 700 -o root -g root /srv/backups/ghost
sudo install -m 700 ghost-backup.sh /usr/local/sbin/ghost-backup.sh
.env를 같이 복사한다면 백업 저장소 접근 권한을 root 또는 백업 운영 계정으로 제한하세요. 리포트나 문서에 DB 비밀번호, SMTP 비밀번호, API 키 본문을 남기면 안 됩니다.
크론 등록
매일 새벽 3시에 실행하려면 root 크론에 등록합니다. 표준 출력과 오류는 로그 파일로 남겨 실패 원인을 추적할 수 있게 합니다.
sudo crontab -e
0 3 * * * /usr/local/sbin/ghost-backup.sh >> /var/log/ghost-backup.log 2>&1
systemd timer를 쓰는 환경이라면 크론보다 실패 상태 확인과 재시도 정책을 관리하기 쉽습니다. 단순 서버라면 크론으로 시작해도 충분합니다.
복구 테스트 절차
백업은 생성보다 복구 검증이 더 중요합니다. 최소 한 번은 임시 디렉터리나 별도 테스트 Compose 프로젝트에서 복구해 봐야 합니다.
mkdir -p /tmp/ghost-restore-test
cd /tmp/ghost-restore-test
tar -tzf /srv/backups/ghost/20260623_030000/ghost-content.tar.gz | head
gzip -t /srv/backups/ghost/20260623_030000/ghost-mysql.sql.gz
sha256sum -c /srv/backups/ghost/20260623_030000/SHA256SUMS
실제 복구에서는 새 DB 컨테이너를 띄운 뒤 SQL을 import하고, content 아카이브를 Ghost content 볼륨에 풀어 넣습니다. 운영 블로그에 바로 덮어쓰기 전에 반드시 테스트 인스턴스에서 URL, 이미지, 테마, 로그인, 게시물 수를 확인하세요.
운영 기준
정기 백업은 “스크립트를 만들었다”에서 끝나면 안 됩니다. 최근 백업 시간, 백업 크기, SQL gzip 무결성, content 압축 해제 가능 여부, 원격 복제 성공 여부를 주기적으로 확인해야 합니다. Ghost Admin JSON export는 이 전체 백업 체계의 보조 산출물로 두고, 복구 기준 백업은 DB와 content를 중심으로 잡는 것이 안전합니다.