본문으로 건너뛰기

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

Quire를 백업하고 시점으로 복구하며 복구 훈련으로 증명하기.

Markdown으로 보기

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

무엇을, 어떻게 보호하는가

자산 방법 위치
데이터베이스 첫 부팅부터 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), 은퇴한 키도 여기에 포함됩니다. 둘 다 백업의 일부입니다.

백업 만들기

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

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

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

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를 실행합니다.

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

호스트 밖의 암호화 사본

두 볼륨 모두 데이터베이스와 같은 호스트에 있고, 실패한 기계의 백업은 백업이 아닙니다. 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). 전송은 멱등합니다. 이미 저장된 것은 건너뛰며, 기준 백업은 마지막에 매니페스트가 쓰여야 저장으로 칩니다.

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

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을 대체하세요.

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

시점 복구

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

  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. 손상된 클러스터를 보관하세요. 복구가 검증될 때까지:
    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. 목표보다 오래된 가장 최신한 기준 백업을 데이터 볼륨에 풀고, 대상 지정 복구를 요청하세요:
    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 오버라이드로 합니다.
    # 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도 포함됩니다.
    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로 확인하세요.

예약된 검증 훈련

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

복구 훈련

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

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 호스트에서 예약으로 실행하세요.

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

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

파일

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

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일간 보관하세요. 그러면 파일의 시점 복구는 버킷 자체의 것이 됩니다.

탐색

검색어를 입력하세요…

↑↓ 이동↵ 선택Esc 닫기