<!-- canonical: https://docs.axelabs.ai/architecture/backup -->
<!-- source: content/architecture/backup.mdx -->

---
title: 백업 · DR
description: restic 3-tier (local · ring P2P · cold SSD), restore drill.
---

# 백업 · DR

## 3-tier 전략

```
Tier A — 로컬 (실시간 보호)
   restic repo /Users/axe/.axe/backups/local/
   매일 03:00 KST, com.axe.backup.local

Tier B — P2P ring (cross-customer 양방향)
   axe-macmini ↔ realchoice-macmini  (SSH key, Tailscale)
   매일 03:30 KST, com.axe.ring.push

Tier C — Cold SSD (offline 보호)
   외장 SSD rotation /Volumes/axe-cold-{1,2,3}
   mount 시 자동 sync
```

## Tier A — 로컬 (live)

| 항목 | 값 |
|---|---|
| Repo path | `/Users/axe/.axe/backups/local/` |
| Restic version | 0.18.1+ |
| Password | macOS Keychain (`axe.backup.restic.local`) |
| 백업 대상 | frame-postgres dump + **blueprint-postgres dump** (D-config-17 cutover 후, 2026-05-15~) + **hive-postgres dump** (D-hive-backup, 2026-05-21~ — Phase 1 조직/휴가 + Phase 3 payroll v2 실데이터) + **mysrt-postgres dump** (B-mysrt-backup-decision, 2026-06-06~ — users/jobs/push_subscriptions; SRT는 train/예약의 SoT일 뿐 계정·잡설정 SoT 아님) + **index-postgres dump** (D-index-51, 2026-06-06~ — `evidence_blob` = 죽은 딜 OneDrive/Blueprint 원본 삭제 후 **유일 사본**, 312MB bytea → ~633MB `pg_dumpall`, restic content-dedup 으로 ~274MB stored) + `.local/files/` (platform data) |
| 빈도 | 매일 03:00 KST (`com.axe.backup.local` launchd) |
| 현재 크기 | ~429 MiB raw-data (2026-06-06 — index-postgres 흡수 후, restic dedup 적용) |
| Excludes | `/Users/axe/.axe/bin/restic-excludes` |

### 명령어

```bash
# 수동 백업
axe backup --local

# 백업 상태 확인
restic -r /Users/axe/.axe/backups/local snapshots \
  --password-file &lt;(security find-generic-password -w -s axe.backup.restic.local)

# 복원 (dry-run)
axe restore --customer axe --from local --as-of 2026-05-20 --dry-run
```

## Tier B — P2P ring

axe ↔ realchoice 양방향 백업 (각자 다른 macmini 의 백업을 보관).

```
axe-macmini → realchoice-macmini 의 /Users/realchoice/peer-backups/axe/
realchoice-macmini → axe-macmini 의 /Users/axe/.axe/backups/peer/realchoice/
```

| 항목 | 값 |
|---|---|
| 전송 | restic `sftp:` backend (SSH key) |
| auth | SSH key (~/.ssh/id_ed25519, 비밀번호 없음) |
| 빈도 | 매일 03:30 KST (`com.axe.ring.push` launchd) |
| 검증 | bidirectional_ssh 2026-05-15 검증 완료 |

`customers.yaml` 에 ring backup 메타 등록:

```yaml
realchoice:
  ring_backup:
    ssh_user: "realchoice"
    ssh_fqdn: "realchoice-macmini.tail090015.ts.net"
    ssh_ip: "100.114.161.51"
    receive_dir_at_peer: "/Users/realchoice/peer-backups/axe/"
    receive_dir_at_self: "/Users/axe/.axe/backups/peer/realchoice/"
    restic_version: "0.18.1"
    disk_available_gib: 43
    bidirectional_ssh: true
```

### 함정

- **Tailscale alone 으로는 충분하지 않음** — SSH key 가 별도 필요. Tailscale 인증 없어도 작동.
- **Cross-customer 백업 = 데이터 노출 risk** — 양측 운영자 모두 합의 + restic 암호화로 컨텐츠 보호.

## Tier C — Cold SSD (offline)

외장 SSD 를 분기별로 rotation. mount 가 감지되면 자동 sync.

| 항목 | 값 |
|---|---|
| Mount path | `/Volumes/axe-cold-{1,2,3}` (rotation) |
| Restic repo | `/Volumes/axe-cold-N/restic-repo/` (N = 1, 2, 3 — rotation index) |
| Password | 종이 메모 (vault 안에 넣지 말 것 — closed-loop 방지) |
| Rotation | 분기별 (Q1 = SSD #1, Q2 = SSD #2, ...) |
| Drill | 분기마다 자동 restore drill (`com.axe.restore-drill`, Jan/Apr/Jul/Oct 15 03:00 KST) |

### 왜 종이?

cold storage 의 password 를 self-host vault 에 넣으면 vault 가 사라졌을 때 cold storage 도 못 풉니다 (closed loop). 종이 + 운영자 머리 = 이중화.

## 복원 절차

### 시나리오 1 — frame DB 손상 (오타 / migration 사고)

```bash
# 1. 어제 03:00 KST 백업 복원
axe restore --customer axe --tier local --as-of 2026-05-20T03:00 --target frame-postgres

# 2. frame 재시작
docker compose restart frame-mcp-blue frame-mcp-green

# 3. 정합성 검사
docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axec
```

### 시나리오 1b — blueprint DB 손상

```bash
# 1. 어제 03:00 KST 백업 복원
axe restore --customer axe --tier local --as-of 2026-05-20T03:00 --target blueprint-postgres

# 2. blueprint app + mcp 재시작 (PR #337 이후 blue/green pair)
docker compose -f /Users/axe/blueprint/docker/docker-compose.yml restart app-green blueprint-mcp-blue blueprint-mcp-green

# 3. 정합성 검사 (Prisma client round-trip)
docker exec blueprint-app-green node -e "const {PrismaClient}=require('@prisma/client'); new PrismaClient().workspace.count().then(n=>console.log('workspaces:',n))"
```

### 시나리오 1c — hive DB 손상

```bash
# 1. 어제 03:00 KST 백업 복원
axe restore --customer axe --tier local --as-of 2026-05-20T03:00 --target hive-postgres

# 2. hive 재시작 (Phase 1 조직/휴가 + Phase 3 payroll v2)
docker compose -f /Users/axe/hive/docker-compose.yml restart hive-postgres hive-mcp-blue hive-mcp-green

# 3. 정합성 검사 (axec/axev 테넌트별 employee count round-trip)
docker exec hive-postgres psql -U hive -d hive -c \
  "SELECT 'axec' AS tenant, COUNT(*) FROM axec.employees
   UNION ALL SELECT 'axev', COUNT(*) FROM axev.employees;"
```

### 시나리오 2 — macmini 자체 손실 (도난, 화재)

```bash
# 1. 새 macmini 셋업 (Tailscale 설치, axe CLI install)
# 2. ring peer 에서 받기
axe restore --customer axe --tier ring --from realchoice --as-of 2026-05-20

# 3. 또는 cold SSD 마운트 + restore
axe restore --customer axe --tier cold --as-of 2026-05-20
```

### 시나리오 3 — 사일런트 corruption

`frame integrity-check --entity axec` 가 detect 하면:

```bash
# 가장 가까운 valid snapshot 으로 부분 복원
axe restore --customer axe --tier local --table journal_line --as-of &lt;last-valid&gt;
```

## Restore drill (자동)

분기마다 자동으로:

1. 임시 staging container 에 어제 backup 복원
2. `frame integrity-check` 통과 여부 확인
3. 결과를 운영자 Slack 으로 보고

drill 자체가 실패하면 cold SSD rotation 시도 또는 ring peer 복원.

## 함정 모음

| 함정 | 결과 | 회피 |
|---|---|---|
| Vault 에 cold storage password 저장 | closed loop, vault 손실 시 cold 도 못 풂 | 종이 메모 |
| restic password 분실 | 백업 영구 손실 | Keychain + 종이 이중화 |
| backup excludes 누락 (`.local/files`) | 핵심 데이터 백업 안 됨 | restic-excludes 명시 |
| `docker exec frame-postgres pg_dump` 만으로 신뢰 | container 죽으면 dump 못 함 | volume 자체 백업 병행 |
| ring backup 단방향 | 한쪽 손실 시 복구 불가 | bidirectional_ssh 검증 필수 |
| index-postgres 백업 누락 | `evidence_blob` = 죽은 딜 OneDrive/Blueprint 원본 삭제 후 **유일 사본** → 영구 소실 | `axe-backup` index 블록 (D-index-51) + `index export-evidence` round-trip drill |
