generated from ttipo/project-base
docs: 서버 B 12 · 13단계 배포에서 겪은 것 — DB digest 확인 · 웹훅 키 먼저 · ZEN 시간 초과 · 손 백업
2026-11-03 서버 B(11단계)를 main 으로 올리며 확인한 것을 차례에 남긴다. - DB 가 이미 고정할 digest 로 돌고 있으면 새 이미지가 없어 날짜를 기다리지 않아도 된다 — 0) 확인 명령 - 12단계 전 서버는 웹훅 키를 먼저 넣는다(키 없이 띄우면 백엔드가 env 검사에서 멈춘다) - --recovery 가 Spamhaus ZEN 시간 초과로 종료 1 이어도 관리자 허용 IP 는 걸렸으니 보통 setup 으로 간다(ZEN 을 다시 켠다) - 운영은 마이그레이션 로그가 없어 migrations 표로 본다 - 손 백업에 DB 덤프를 더하고, 떠 있는 alpine 대신 지금 도는 DB 이미지 · 읽기 권한만 · 0600 으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-7
@@ -959,13 +959,17 @@ unset OTP_ADMIN_PW OTP_TARGET
|
||||
### 13단계 전부터 운영 중인 서버
|
||||
|
||||
이미 떠 있는 운영 서버(서버 B)에 13단계를 거는 차례다. **2026-11-04 뒤에** 한다 — 고정한 DB 이미지(digest)가 그날 2주가 찬다(`.env.production.example` 의 `DB_IMAGE` 주석).
|
||||
서버의 DB 가 이미 그 digest 로 돌고 있으면(아래 0) 새로 들이는 이미지가 없어 날짜를 기다리지 않아도 된다 — 서버 B 는 10-31 에 받은 `postgres:15-alpine` 이 같은 digest 라 2026-11-03 에 올렸다.
|
||||
**12단계 전부터 운영 중인 서버**(웹훅 키가 없다)는 3) 전에 [위 12단계 차례](#운영-배포)의 1) — `MAIL_WEBHOOK_SECRET` 넣기 — 를 먼저 한다(키 없이 3)을 하면 백엔드가 env 검사에서 멈춰 웹메일 전체가 선다). 웹훅은 5)의 setup 이 만든다 — 서버 B 는 11단계에서 이렇게 12 · 13단계를 한 번에 올렸다.
|
||||
모든 컨테이너가 다시 만들어진다(로그 한도 · DB 이미지) — 메일 · 웹메일이 1~2분 끊기므로 사용이 적은 시간에 한다. 그 전에 손으로 백업을 받아 두기를 권한다([손으로 받을 때](#손으로-받을-때-타이머-없이--예전-방법)).
|
||||
|
||||
```bash
|
||||
cd ~/arasystem && git pull # 13단계 코드 (저장소 사용자로)
|
||||
export PROJECT_NAME="$(sed -n 's/^PROJECT_NAME=//p' .env.production)"
|
||||
# 1) 로그 남기기 — 컨테이너를 다시 만들면 그때까지의 로그가 지워진다
|
||||
mkdir -p ~/logs-before-13 && for c in mail backend frontend db; do sudo docker logs "${PROJECT_NAME}-$c" > ~/logs-before-13/$c.log 2>&1; done
|
||||
# 0) 지금 DB 가 쓰는 이미지 — 예시의 DB_IMAGE 와 같은 sha256 이면 날짜를 기다리지 않아도 된다(위)
|
||||
sudo docker image inspect "$(sudo docker inspect -f '{{.Image}}' "${PROJECT_NAME}-db")" --format '{{json .RepoDigests}}'
|
||||
# 1) 로그 남기기 — 컨테이너를 다시 만들면 그때까지의 로그가 지워진다(메일 주소 · IP 가 들어 있다 — 이 사용자만 읽게)
|
||||
umask 077 && mkdir -p ~/logs-before-13 && for c in mail backend frontend db; do sudo docker logs "${PROJECT_NAME}-$c" > ~/logs-before-13/$c.log 2>&1; done
|
||||
# 2) .env.production 의 DB_IMAGE 를 예시와 같은 고정값으로 (옛 값 postgres:15-alpine) — 편집기로 그 한 줄만
|
||||
grep -n '^DB_IMAGE=' .env.production .env.production.example
|
||||
# 3) 복구 관리자를 일회성으로 준 채 전부 다시 만든다 — 관리자 허용 IP 를 처음 걸 때는 복구 관리자가 있어야 setup 이 건다(막히면 되돌릴 길)
|
||||
@@ -984,6 +988,10 @@ node --env-file=.env.production mail/scripts/setup.mjs
|
||||
- 5)의 출력에 `[변경] 관리자 역할("System Administrator") — 대리 접근(impersonate) 뺌`, `[변경] 관리자 … — 허용 IP <APP_NETWORK_SUBNET>`, 휴지통 · 정리 시각 줄,
|
||||
그리고 `[확인] 관리자 … — 대리 접근 권한 없음 (권한 N개에 impersonate 없음) · 허용 IP … 안에서 로그인됨` 이 있어야 한다. 6)은 모두 `[유지]` 에 같은 `[확인]` 줄.
|
||||
- 복구 관리자 없이 처음 돌리면 setup 이 허용 IP 를 걸지 않고 종료 1 로 이 절을 가리킨다(아무것도 막히지 않는다 — 다시 3)부터).
|
||||
- 5)가 종료 1 이고 `[실패]` 가 **Spamhaus ZEN 시험 항목의 시간 초과**뿐이면 — 리졸버를 막 다시 띄운 직후에 난다(2026-10-31 · 11-03 두 번, 같은 키의 DBL 은 통과).
|
||||
setup 이 ZEN 을 꺼 두지만 6)의 보통 setup 이 다시 시험해 켠다(`[변경] … ZEN(IP) — DQS 로 묻는다`). 관리자 허용 IP 는 이미 걸리고 확인됐으니(`[확인] 관리자 … 안에서 로그인됨`) 그대로 6)으로 간다 — 이때는 "복구 관리자 끄기" 안내가 나오지 않는다.
|
||||
- 6)의 끝에 `[확인] 복구 관리자 꺼짐 (mail 컨테이너에 STALWART_RECOVERY_ADMIN 없음)` · `[완료]` · 종료 0 이면 끝이다(2026-11-03 서버 B — 백엔드는 마이그레이션 `AddAccountSecurity` · `AddCutoverPrep` 를 적용하며 떴다. 운영은 마이그레이션 로그를 남기지 않으니 `migrations` 표로 본다:
|
||||
`sudo docker exec "${PROJECT_NAME}-db" sh -c 'exec psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "select name from migrations order by id desc limit 3"'`).
|
||||
- 7) **백업 · 감시 설치** — [백업](#백업) › 설치 1~4(부팅 때 엔진 되살리기 유닛 포함), 첫 백업 뒤 `restore.sh drill` 한 번, [감시](#감시-13단계)의 Healthchecks · UptimeRobot.
|
||||
- 8) 바깥에서 관리자 비밀번호가 막혔는지 — 다른 네트워크의 PC 에서 `openssl s_client -quiet -connect mx1.arasystem.it.kr:993` 뒤 `a LOGIN <관리자 주소> <비밀번호>` → `NO`(또는 끊김).
|
||||
관리 화면은 이제 [SSH 터널](#관리-화면--ssh-터널)로만 들어간다.
|
||||
@@ -1807,14 +1815,20 @@ systemctl list-timers 'arasystem-*'
|
||||
|
||||
### 손으로 받을 때 (타이머 없이 — 예전 방법)
|
||||
|
||||
배포 전 등에 한 번 받을 때 — DB 는 멈추지 않고, 메일 엔진만 30초쯤 멈춘다(2026-11-03 서버 B 13단계 배포 전에 이대로 받았다 — DB 표 20개 · 메일 1MB).
|
||||
|
||||
```bash
|
||||
sudo docker compose --env-file .env.production stop mail
|
||||
sudo docker run --rm -v "${PROJECT_NAME}_mail-config:/config:ro" -v "${PROJECT_NAME}_mail-data:/data:ro" -v "$PWD/backup:/backup" \
|
||||
alpine tar czf "/backup/mail-$(date +%Y%m%d-%H%M).tar.gz" -C / config data
|
||||
sudo docker compose --env-file .env.production start mail
|
||||
cd ~/arasystem && export PROJECT_NAME=arasystem && umask 077 && D=~/backup-$(date +%Y%m%d-%H%M) && mkdir -p "$D"
|
||||
# DB — 백업 스크립트와 같은 방법(pg_dump -Fc 뒤 pg_restore -l 로 읽히는지 — 표 수가 나온다)
|
||||
sudo docker exec "${PROJECT_NAME}-db" sh -c 'exec pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > "$D/db.dump" && sudo docker exec -i "${PROJECT_NAME}-db" pg_restore -l < "$D/db.dump" | grep -c ' TABLE '
|
||||
# 메일 볼륨 — 엔진을 멈추고 묶는다. 이미지는 지금 도는 DB 이미지(새로 받지 않는다) · 네트워크 없음 · 읽기 권한만, 파일은 표준 출력으로 받아 이 사용자 것(0600)
|
||||
IMG="$(sudo docker inspect -f '{{.Image}}' "${PROJECT_NAME}-db")" && sudo docker compose --env-file .env.production stop mail && sudo docker run --rm --network none --cap-drop ALL --cap-add DAC_READ_SEARCH --security-opt no-new-privileges -v "${PROJECT_NAME}_mail-config:/config:ro" -v "${PROJECT_NAME}_mail-data:/data:ro" --entrypoint tar "$IMG" czf - -C / config data > "$D/mail.tar.gz"; echo "tar 종료 코드 $?"; sudo docker compose --env-file .env.production start mail
|
||||
ls -lh "$D"
|
||||
```
|
||||
|
||||
- 복원은 빈 볼륨에 같은 경로로 풀고(`tar xzf … -C /`, 소유자 UID 2000 유지) 컨테이너를 띄운다. 백업 파일에는 메일 전체가 들어 있다 — 접근을 제한하고 서버 밖에도 둔다.
|
||||
- 메일 볼륨 줄은 한 줄이다 — 앞 단계가 실패하면 엔진을 멈추지 않게 `&&` 로 묶었다(나눠 붙이다 `IMG=` 를 빠뜨리면 `invalid reference format` 에 빈 파일이다 — 2026-11-03 한 번 겪음). `start mail` 은 어떻게 끝나도 돈다.
|
||||
- 이미지를 따로 받지 않는 까닭 — 떠 있는 태그(`alpine` 등)를 쓰면 그날의 새 이미지가 들어온다([docker 스킬](../.claude/skills/docker/SKILL.md) "이미지 버전 고정").
|
||||
- 복원은 DB 는 빈 DB 에 `pg_restore`, 메일은 빈 볼륨에 같은 경로로 풀고(`tar xzf … -C /`, 소유자 UID 2000 유지) 컨테이너를 띄운다. 백업 파일에는 메일 전체 · DKIM 키 · DB 가 들어 있다 — 접근을 제한하고 서버 밖에도 두며, 쓸모가 끝나면 지운다.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user