diff --git a/mail/README.md b/mail/README.md index d8678fb..408d098 100644 --- a/mail/README.md +++ b/mail/README.md @@ -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 `, 휴지통 · 정리 시각 줄, 그리고 `[확인] 관리자 … — 대리 접근 권한 없음 (권한 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 가 들어 있다 — 접근을 제한하고 서버 밖에도 두며, 쓸모가 끝나면 지운다. ---