# 백업·복원 운영 가이드

학습 플랫폼 **DB(PostgreSQL)**와 **Git 서버(Gitea)**가 **매일 새벽** 자동 백업되어 S3에 올라갑니다.

## 구성

```
EC2 (3.36.160.246)
├── mirim-postgres 컨테이너   ← 원본 DB
├── mirim-gitea 컨테이너      ← 원본 Git 서버 (저장소 + SQLite + 설정)
├── ~/backups/                ← 서버 로컬 사본 (7일 보관)
└── systemd timer
      ├── 03:00 KST → backup-db.sh     : pg_dump | gzip | s3 cp
      └── 03:10 KST → backup-gitea.sh  : gitea dump | s3 cp   (10분 뒤 — 겹치지 않게)
              ↓
S3: awesomedev-mirim-backup-apne2/
    ├── postgres/*.sql.gz   ← 30일 보관 후 자동 삭제
    └── gitea/*.zip         ← 30일 보관 후 자동 삭제
```

**3-2-1 원칙**: 3벌(원본·로컬·S3) / 2종 매체(EC2 디스크·S3) / 1벌 원격(S3)

## 왜 이렇게 만들었나

| 결정 | 이유 |
|---|---|
| **RDS 대신 컨테이너 + 백업** | 학생 5명 규모에 RDS는 월 $15~25가 계속 나감. 백업만 있으면 실질 위험은 같이 사라진다. 게다가 이 compose 파일이 "도커 입문" 코스의 교재다 |
| **액세스 키 대신 IAM 역할** | 키를 파일에 적으면 유출 시 끝. 역할(`mirim-backup-role`)은 이 서버에서만, 이 버킷의 `postgres/` 에만 쓸 수 있다 (최소 권한) |
| **cron 대신 systemd timer** | Amazon Linux 2023에 cron이 없기도 하고, 타이머는 실행 이력이 journalctl에 남고 `Persistent=true`로 서버가 꺼졌던 시간대 작업도 따라잡는다 |
| **SQL 덤프(pg_dump)** | 파일 복사가 아니라 "다시 만드는 SQL"이라 PG 버전이 달라도 복원되고, 텍스트라 gzip이 잘 먹는다 (8.5MB → 102KB) |
| **Gitea는 볼륨 tar가 아니라 `gitea dump`** | Gitea는 저장소 파일 + SQLite DB(이슈·PR·계정) + 설정이 한 몸이다. 돌아가는 중에 tar로 묶으면 "파일은 새 것, DB는 옛 것"처럼 시점이 어긋날 수 있다. `gitea dump`는 셋을 일관된 시점으로 묶는다. → **앱이 자기 백업 도구를 제공하면 그걸 먼저 찾아본다** |

## 자주 쓰는 명령

```bash
# 백업 목록 보기 (로컬 + S3)
~/mirim-app/deploy/restore-db.sh

# 지금 즉시 백업
sudo systemctl start mirim-backup.service         # DB
sudo systemctl start mirim-gitea-backup.service   # Gitea

# 백업 이력·로그 확인
sudo journalctl -u mirim-backup.service -n 30
sudo journalctl -u mirim-gitea-backup.service -n 30

# 다음 실행 예정 시각 (둘 다)
systemctl list-timers 'mirim-*'

# DB 복원 (확인 프롬프트 있음. 복원 전 현재 상태를 자동으로 먼저 백업함)
~/mirim-app/deploy/restore-db.sh mirim-2026-07-17_030000.sql.gz
docker compose -f docker-compose.prod.yml restart backend   # 복원 후 재시작
```

## Gitea 복원 방법

Gitea 복원은 자주 쓸 일이 아니고 단계가 많아 스크립트 대신 절차로 둔다.
(자동화하지 않는 것도 선택이다 — 드물게 쓰는 위험한 작업은 사람이 한 단계씩 확인하는 편이 안전하다.)

```bash
# 1) 백업 받아서 풀기
aws s3 cp s3://awesomedev-mirim-backup-apne2/gitea/gitea-2026-07-17_031000.zip /tmp/
cd /tmp && unzip -q gitea-2026-07-17_031000.zip -d gitea-restore

# 2) Gitea 중지
cd ~/mirim-app/deploy && docker compose -f docker-compose.prod.yml stop gitea

# 3) 저장소·설정·DB 되돌리기 (볼륨에 직접 씀 — sudo 필요)
V=/var/lib/docker/volumes/deploy_gitea-data/_data
sudo cp -a /tmp/gitea-restore/repos/.        $V/git/repositories/
sudo cp -a /tmp/gitea-restore/data/.         $V/gitea/
sudo cp    /tmp/gitea-restore/app.ini        $V/gitea/conf/app.ini
# gitea-db.sql → SQLite로 되돌리기
sudo rm -f $V/gitea/gitea.db
docker run --rm -v deploy_gitea-data:/data -v /tmp/gitea-restore:/r alpine/sqlite \
  sh -c "sqlite3 /data/gitea/gitea.db < /r/gitea-db.sql"
sudo chown -R 1000:1000 $V

# 4) 다시 시작
docker compose -f docker-compose.prod.yml start gitea
```

> 저장소만 급히 살리면 되는 경우엔 훨씬 간단하다 — zip 안의 `repos/awesomedev/mirim-app.git`은
> 그 자체로 완전한 bare 저장소라, `git clone repos/awesomedev/mirim-app.git` 하면 바로 소스가 나온다.
> **실제로 이 방법으로 복원 검증을 했다** (아래 참고).

## 복원 리허설 (권장: 분기 1회)

> **복원해 본 적 없는 백업은 백업이 아니다.**

운영 DB를 건드리지 않고 임시 DB에 복원해 검증하는 방법:

```bash
F=$(ls -t ~/backups/mirim-*.sql.gz | head -1)
docker exec mirim-postgres psql -U mirim -d postgres -c 'CREATE DATABASE restore_test;'
gunzip -c $F | docker exec -i mirim-postgres psql -U mirim -d restore_test
# 건수 대조
docker exec mirim-postgres psql -U mirim -d restore_test -c \
  "SELECT count(*) FROM users; SELECT count(*) FROM quiz_question;"
docker exec mirim-postgres psql -U mirim -d postgres -c 'DROP DATABASE restore_test;'
```

Gitea도 같은 방식으로 검증할 수 있다 (백업 속 저장소에서 직접 clone):

```bash
F=$(ls -t ~/backups/gitea-*.zip | head -1)
mkdir -p ~/gtest && cd ~/gtest && unzip -qo $F 'repos/*'
docker run --rm --entrypoint sh -v ~/gtest:/w -w /w alpine/git:latest -c '
  git config --global --add safe.directory "*"
  git clone -q repos/awesomedev/mirim-app.git restored && cd restored
  git log --oneline -3; git rev-list --count HEAD'
sudo rm -rf ~/gtest
```

**최초 검증 결과 (2026-07-17)**

| 대상 | 결과 |
|---|---|
| PostgreSQL | 임시 DB에 복원 → 운영 원본과 **완전 일치** (사용자 5 / 퀴즈 355 / 문서 17 / 체크리스트 46 / 코딩 20) ✅ |
| Gitea | 백업 속 저장소에서 clone 성공 → HEAD·커밋 20개·파일 269개가 운영과 **완전 일치** ✅ |

## 무엇이 백업되고, 무엇이 안 되나

| 대상 | 백업 | 비고 |
|---|---|---|
| 학생 진도·제출물·퀴즈 성적·코딩 제출 | ✅ postgres | **이게 진짜 자산** — 코드에 없는 유일한 데이터 |
| 플랫폼 사용자 계정(비밀번호 해시) | ✅ postgres | |
| 코스·문서·퀴즈 문항·체크리스트 | ✅ postgres | (없어도 시드로 재생성 가능) |
| Git 저장소(소스·전체 이력) | ✅ gitea | 개발자 PC에도 clone 사본이 있어 이중 안전 |
| **Gitea 이슈·PR 리뷰 코멘트** | ✅ gitea | **Gitea에만 있는 것** — 잃으면 복구 불가 |
| Gitea 계정·권한·설정 | ✅ gitea | |
| EC2 서버 설정 자체 | ❌ | 재구축은 `deploy/` + `DEPLOY` 문서로 가능 |

> 백업 설계의 출발점은 **"무엇이 이 서버에만 있는가"**를 묻는 것이다.
> 소스 코드는 git의 분산 구조 덕에 개발자 PC마다 사본이 있지만,
> **이슈·PR 코멘트·학생 제출물**은 이 서버에만 있다 — 그래서 이 둘을 지킨다.

## 비용

| 항목 | 크기 | 비용 |
|---|---|---|
| postgres 백업 | 102KB × 30일 ≈ 3MB | ~$0 |
| gitea 백업 | 2.8MB × 30일 ≈ 84MB | ~$0.002/월 |
| 업로드 전송 | — | 무료 |
| **합계** | ~87MB | **월 몇 원 (≈$0)** |
