---
title: "백업, 시점 복구, 복구 훈련"
description: "Quire를 백업하고 시점으로 복구하며 복구 훈련으로 증명하기."
image: "https://docs.quirelms.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.quirelms.com/ko/llms.txt
> Use this file to discover all available pages before exploring further.

# 백업, 시점 복구, 복구 훈련

<span id="backup-point-in-time-recovery-and-the-restore-drill"></span>

설계는 `docs/architecture/23-ops.md` 8절입니다. 이것은 Docker Compose
제품의 런북입니다. 이 문서를 쓰지 않은 사람이 따라할 수 있도록 쓰여
있습니다. 단계가 명확하지 않다면 그것은 이 문서의 결함입니다.

## 무엇을, 어떻게 보호하는가 <!--quire:what-is-protected-and-how-->

| 자산 | 방법 | 위치 |
| --- | --- | --- |
| 데이터베이스 | 첫 부팅부터 WAL을 연속 보관. 최대 60초 간격 | `pgwal` 볼륨 |
| 데이터베이스 | `pg_basebackup`으로 기준 백업. 기본은 매일(`backup-scheduler`) | `pgbackup` 볼륨 |
| 데이터베이스 | 기준 백업과 WAL의 암호화 사본. 5분마다(`backup-offsite`) | 직접 지정하는 별도 저장소 |
| 파일 | `files` 볼륨. 호스트의 백업 도구로 복사하거나 버전 관리 객체 저장소 사용 | `files` 볼륨 |
| 시크릿 | `docker/.env`, 그중에서도 `QUIRE_MASTER_KEY`(사용 중인 모든 `QUIRE_MASTER_KEY_RETIRED` 포함), `QUIRE_BACKUP_ENCRYPTION_KEY`, `docker/secrets/audit-signing-key.pem` | 이 호스트 밖에 사본 보관 |
| 검색 색인, 캐시, 렌더링 | 백업하지 않음; 다시 구축 | |

목표: 실패 시점에서 60초 이내의 복구 지점, 500GB 데이터베이스는 60분 이내의
복구.

흔한 실수는 두 가지입니다. **파일 없이** 복구된 데이터베이스는 깨진
페이지를 렌더링합니다. **`QUIRE_MASTER_KEY` 없이** 복구된 데이터베이스는
보관 중인 SSO, 웹훅, 연동 자격 증명을 복호화할 수 없습니다. 마스터 키
로테이션이 미해결 없이 끝날 때까지([key-rotation.md](/ko/ops/key-rotation/)),
은퇴한 키도 여기에 포함됩니다. 둘 다 백업의 일부입니다.

## 백업 만들기 <!--quire:taking-backups-->

클러스터 전체의 기준 백업:

```sh
docker compose -f docker/compose.yaml --profile backup run --rm backup
```

가장 최신한 `QUIRE_BACKUP_KEEP`개의 기준 백업(기본 5)을 남기고, 가장 오래된
것이 더 이상 필요로 하지 않는 WAL은 정리하므로 보관함이 무한히 커지지
않습니다. 호스트의 cron이나 systemd 타이머로 매일 예약하세요.

```cron
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1
```

또는 스택이 예약하게 하세요. `backup` 프로필은 `backup-scheduler`(매
`QUIRE_BACKUP_INTERVAL_HOURS`마다 기준 백업, 기본 24)와 다음에 설명할
`backup-offsite`를 실행합니다.

```sh
docker compose -f docker/compose.yaml --profile backup up -d
```

## 호스트 밖의 암호화 사본 <!--quire:encrypted-off-host-copies-->

두 볼륨 모두 데이터베이스와 같은 호스트에 있고, 실패한 기계의 백업은
백업이 아닙니다. `backup-offsite`는 모든 기준 백업과 보관된 WAL 세그먼트를
스토리지 포트를 통해 암호화된 별도 저장소로 복사하고, 보존 규칙에 따라
거기에 보관합니다.

- **암호화.** `QUIRE_BACKUP_ENCRYPTION_KEY`(또는
  `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`이 지정하는 파일)로 하는 AES-256-GCM.
  32바이트이며 `openssl rand -hex 32`로 만듭니다. 각 파일은 고유한 nonce와
  인증 태그를 가지므로 키 없이는 읽을 수 없고 어떤 변경도 감지됩니다. 키는
  `QUIRE_MASTER_KEY`와 함께, 이 호스트와 백업 저장소 모두에서 멀리 두세요.
  키가 없으면 복구는 없습니다.
- **어디에.** `QUIRE_BACKUP_STORAGE_DRIVER`는 `s3`, `azure`, `local`
  (`QUIRE_BACKUP_STORAGE_ROOT`의 마운트된 원격 디스크) 중 하나입니다. 설정은
  파일 저장소 설정에 `QUIRE_BACKUP_` 접두어를 붙인 것입니다.
  `QUIRE_BACKUP_S3_ENDPOINT`, `QUIRE_BACKUP_S3_BUCKET`,
  `QUIRE_BACKUP_S3_ACCESS_KEY_ID` 등이 있습니다. 파일과 다른 버킷을, 가능하면
  다른 계정을 쓰고, 제공자가 허하면 쓸 수는 있으나 삭제할 수 없는 자격
  증명을 쓰세요.
- **보존.** 가장 최신한 `QUIRE_BACKUP_OFFSITE_KEEP`개의 기준 백업(기본은
  `QUIRE_BACKUP_KEEP`, 없으면 7)과 그중 가장 오래된 것이 필요로 하는 WAL.
  더 오래된 세트와 세그먼트는 저장소에서 삭제됩니다.
- **언제.** `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS`마다(기본 300). 전송은
  멱등합니다. 이미 저장된 것은 건너뛰며, 기준 백업은 마지막에 매니페스트가
  쓰여야 저장으로 칩니다.

같은 명령을 직접 실행할 수도 있습니다.

```sh
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify
```

새 호스트에서 복구하려면 먼저 세트를 가져온 뒤, 아래 단계를 따라가되
내려받은 디렉터리로 `pgbackup` 볼륨을, 내려받은 `wal-archive`로 `pgwal`을
대체하세요.

```sh
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
```

## 시점 복구 <!--quire:restoring-to-a-point-in-time-->

데이터 손실 뒤에 사용하세요. 잘못된 가져오기, 삭제된 코스, 되돌려야 하는
축소 마이그레이션이 그렇습니다. 라이브 데이터베이스를 대체하므로, 먼저
아래 훈련으로 연습하세요.

1. **목표 시각을 고르세요.** UTC로, 손상 직전 시점입니다.
   `2026-09-24 09:30:00+00`. 감사 로그(`/admin/audit`)가 그 순간을 대개
   보여줍니다.
2. **쓰는 것을 모두 멈추세요**:
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **손상된 클러스터를 보관하세요.** 복구가 검증될 때까지:
   ```sh
   docker compose -f docker/compose.yaml stop postgres
   docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
   ```
4. **목표보다 오래된 가장 최신한 기준 백업을 데이터 볼륨에 풀고**,
   대상 지정 복구를 요청하세요:
   ```sh
   docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc '
     base="$(ls -1d /backups/base-* | sort | tail -n 1)"   # or the one before the target
     rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker
     tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker
     touch /var/lib/postgresql/18/docker/recovery.signal
     chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker'
   ```
5. **복구하세요**: 복구 설정과 함께 Postgres를 한 번 시작합니다. 일반 파일을
   건드리지 않도록 Compose 오버라이드로 합니다.
   ```yaml
   # docker/compose.recover.yaml
   services:
     postgres:
       command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p",
                 -c, "recovery_target_time=2026-09-24 09:30:00+00",
                 -c, recovery_target_action=promote, -c, archive_mode=off,
                 -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]
   ```
   오버라이드는 명령 전체를 대체하므로 복구가 의존하는 두 설정을 반복합니다.
   `max_connections`는 주 서버보다 낮지 않아야 하며(그렇지 않으면
   "insufficient parameter settings"로 복구가 중단됩니다), 마운트된
   `pg_hba.conf`도 포함됩니다.
   ```sh
   docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres
   docker compose -f docker/compose.yaml logs -f postgres   # wait for "database system is ready"
   ```
6. **아무도 들어가기 전에 확인하세요.** 감사 체인
   (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`)과
   잃어버린 데이터가 돌아왔는지 확인합니다.
7. **정상으로 돌아오세요**: `docker compose -f docker/compose.yaml up -d`.
   아카이빙을 켠 채 Postgres를 재시작하며 새 WAL 타임라인이 시작됩니다.
   곧바로 새로운 기준 백업을 만드세요.

프로젝트 이름 `quire`가 각 볼륨의 접두어가 됩니다. 정확한 이름은
`docker volume ls`로 확인하세요.

## 예약된 검증 훈련 <!--quire:the-scheduled-verification-drill-->

`backup-offsite`는 `QUIRE_BACKUP_DRILL_INTERVAL_HOURS`마다(기본 168, 주
1회) 훈련도 실행하며, 하나가 실패하면 다음 주기에 다시 실행합니다. 최신한
호스트 밖 기준 백업과 그 뒤의 모든 WAL 세그먼트를 가져와 각각을 복호화하고
(키가 여전히 열리고 아무것도 변조되지 않았음을 증명), 각 파일을 매니페스트와
비교하며, 보관함이 Postgres 데이터 디렉터리인지 확인하고, 백업 이후의 WAL에
공백이 없는지 확인합니다. 보고서는 `reports/drill-<time>.json`으로 저장소에
그리고 서비스 로그에 쓰입니다. 실패한 훈련은 파일 이름이나 처음 누락된
세그먼트를 명시합니다.

## 복구 훈련 <!--quire:the-restore-drill-->

한 번도 복구되지 않은 백업은 백업이 아닙니다. 훈련은 목표 이전에 만든
최신한 완전한 기준 백업과 WAL 보관함을, 라이브와 아무것도 공유하지 않는
임시 Postgres에 복구하고 그 결과를 증명합니다.

```sh
docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"
```

목표는 정확히 그 형태의 UTC입니다. 목표보다 오래된 기준 백업과 그 이후의
보관된 WAL이 필요합니다. 새 설치라면 기준 백업을 만들고 다음 보관 세그먼트
(쓰기가 있으면 최대 1분)를 기다린 뒤, 백업 이후의 시각을 목표로 고르세요.
훈련에는 호스트의 Docker와 bash만 필요합니다.

각 단계는 훈련을 실패시킵니다.

1. **복구 가능성**: 임시 클러스터가 목표까지 재생되고 열리는지.
2. **완전성**: 라이브 데이터베이스와 각 테이블의 행 수 비교
   (`tooling/restore-drill`). 라이브 데이터베이스는 목표 이후로 진행되었으므로
   테이블은 500행과 크기의 10분의 1 중 큰 만큼, 어느 방향으로든 다를 수
   있습니다(쓰기는 뒤처지게 하고, 삭제는 복구본이 더 많게 합니다). 목표
   이후에 만들어진 파티션은 잃어버린 테이블이 아닙니다. 더 바쁜 설치에서는
   `QUIRE_DRILL_MAX_BEHIND`와 `QUIRE_DRILL_MAX_DRIFT_RATIO`로 허용치를
   넓히세요. 사라졌거나 비어 있는 테이블은 실패합니다.
3. **무결성**: 복사본에서 감사 해시 체인이 검증되는지.
4. **유용성**: 애플리케이션 역할이 행 수준 보안을 통해 읽는지.
5. **시간**: 시작부터 초록까지, `QUIRE_DRILL_RTO_SECONDS`(기본 3600) 대비.

라이브 데이터베이스나 그 볼륨에는 절대 쓰지 않습니다. 백업과 WAL 볼륨은
읽기 전용으로 마운트되며, 임시 클러스터는 통과·실패와 무관하게 끝에
삭제됩니다.

`QUIRE_DRILL_REPORT`에 경로를 지정하면 통과·실패와 무관하게 JSON 보고서가
쓰이며, Docker 호스트에서 예약으로 실행하세요.

```cron
30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1
```

매달, 그리고 모든 업그레이드 전에 실행하세요. 실패한 훈련은 업그레이드를
막습니다. 분기마다, 이 런북을 쓰지 않은 사람에게 예비 호스트에서 이 문서만
사용해 실제 시점 복구를 수행하게 하세요.

## 파일 <!--quire:files-->

로컬 파일은 `files` 볼륨에 있습니다. 데이터베이스와 같은 시점에 백업하고
둘 다 함께 복구하세요.

```sh
docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .
```

객체 저장소를 쓴다면 버킷 버전 관리를 켜고 비현재 버전을 35일간 보관하세요.
그러면 파일의 시점 복구는 버킷 자체의 것이 됩니다.

Source: https://docs.quirelms.com/ko/ops/backup-restore/index.mdx
