fix(docker): mail setup 받는 메일 크기 한도 50MiB · 전환 준비 문서 (mailcow 확인 · 서버 B 배포 · 백업 · 큰 메일) #12

Merged
ttipo merged 14 commits from feat/mail-migration-password into main 2026-11-04 07:59:25 +09:00
Owner

무엇을 · 왜

PR #11(메일 이전)을 합친 뒤 운영 mailcow 를 직접 확인한 결과로 mail/README.md 의 imapsync 절을 바로잡고, 같은 날 서버 B 를 12 · 13단계로 올리며 겪은 것을 배포 차례에 남겼다. 그리고 이전 1 · 2차에서 나온 큰 메일 문제로 setup 이 SMTP 받는 메일 크기 한도를 맞추게 했다(6dc2777 — 나머지 커밋은 문서만).

  • mailcow 호스트는 사무실 PC 의 WSL(/opt/mailcow-dockerized — mailcow.conf · 저장소가 root 소유) — 확인 · 마스터 계정 명령을 sudo 와 전체 경로로. git 의 safe.directory 는 건드리지 않는다.
  • 마스터 두 줄은 기본이 빈 값 — mailcow 의 generate_config.sh 가 DOVECOT_MASTER_USER= · DOVECOT_MASTER_PASS= 를 넣어 두므로 "주석이 아니면 정해 둔 것"이 아니다(운영도 둘 다 빈 값). 확인은 값 대신 비었는지만 보고, 이전을 마칠 때는 줄을 지우지 않고 값을 비운다.
  • 준비에 "원본 로그인만 미리" — 대상 계정 없이 두 쪽 모두 mailcow 로 --justlogin. 단계 2(계정 만들기)부터는 짧게 이어야 하므로 원본 쪽 문제(마스터 · 인증서 · 앞단 길)를 그 전에 고친다.
  • mailcow 는 바깥 접속을 모두 앞단 터널 주소 10.9.0.1(사설)로 본다 — 최근 7일 로그인 줄과 지금 연결로 확인(공인 주소 0). mailcow 의 Fail2ban 은 사설 · 루프백 주소를 막지 않으므로(원본 netfilter/main.py) 서버 B 를 예외에 넣을 필요가 없다. 남는 것은 Dovecot 의 늦추기(실패한 주소의 다음 로그인 최대 15초 — 바깥 직원 모두와 같은 주소)라 실패를 되풀이하지 않는다.
  • 이 mailcow 의 docker 로그(json-file · 회전 없음 · Dovecot 만 428MB)는 --since 만 주면 0줄이다(처음부터 읽다 중간에서 말없이 멈추는 것으로 보인다) — 로그 명령은 모두 --tail 로, 로그 없이 지금 연결을 보는 doveadm who 를 더했다.
  • 운영 사실: mailcow 2026-05-12 판(32156a33), Dovecot 2.3 줄기.
  • 운영 imapsync 의 큰 메일(2026-11-03 1 · 2차 — 15계정) — 엔진 크기 한도(관리 화면 세 값 → 110 MiB, setup 은 맞추지 않는다)보다 큰 메일은 APPEND 에서 빠지고(114), 100MB 남짓 한 통은 엔진 1g 를 넘겨 OOM 으로 다시 떴다 — docker update 2g 뒤 다시 돌려 통과. 110 MiB 를 넘는 초안 두 통은 옮기지 못했다(사용자가 따로 보관). 이전 뒤 한도 · 메모리를 어떻게 둘지는 아래 6dc2777 에서 정했다.
  • SMTP 받는 메일 크기 한도 50MiB(6dc2777 — 개발 엔진 실측 · 사용자 결정): 큰 메일 한 통에 엔진 메모리가 그 크기의 5.5배쯤 들고, SMTP 받는 한도(x:MtaStageData 의 maxMessageSize, 기본 100MiB)는 x:Email 한도와 따로라 기본값 · 1g 에서 바깥 104MB 한 통이 984/1024MiB 였다.
    메모리는 1g 그대로 두고 setup 이 받는 한도를 50MiB 로 맞춘 뒤 리로드 뒤 EHLO 의 SIZE 로 확인한다([확인] 받는 메일 크기 한도 — EHLO SIZE …). IMAP · 메일 크기 한도는 이전 동안 올리므로 바꾸지 않고 기본보다 크면 [주의] 만.
    운영 전환 7번은 이전을 마치면 세 값을 관리 화면에서 기본으로 → setup 의 [확인] → 메모리 1g 차례다 — 이 PR 을 합친 뒤 서버 B 에서 git pull 해 둔다.
커밋 내용
e664217 docs mailcow 호스트는 사무실 PC 의 WSL — 확인 · 마스터 계정 명령을 sudo 로
34d63cf docs 마스터 두 줄은 기본이 빈 값(확인 명령 바로잡기 · 끝낼 때 값 비우기) · 준비의 "원본 로그인만" · mailcow 가 보는 접속 주소
b43b747 docs 운영 확인 결과 — 바깥 접속은 모두 10.9.0.1 이라 Fail2ban 예외 필요 없음 · 로그는 --tail · doveadm who
07b7fcc docs 서버 B 12 · 13단계 배포(2026-11-03)에서 겪은 것 — DB digest 가 같으면 날짜를 기다리지 않음 · 12단계 전 서버는 웹훅 키 먼저 · --recovery 의 Spamhaus ZEN 시간 초과(종료 1)는 보통 setup 이 다시 켬 · 마이그레이션은 migrations 표로 · 손 백업에 DB 덤프(떠 있는 alpine 대신 지금 도는 DB 이미지)
0388cf6 docs restic 비밀번호는 비밀번호 관리자에서 먼저 만들어 화면 · 셸 기록 없이 서버에 넣기(설치 2 · ops/ops.env.example 머리말) — 서버 밖 사본이 백업보다 먼저 생긴다
feabe0d docs 백업 버킷 만들기 — 공개 안 함 · 잠금 해제(보존 기간은 prune 을 막는다) · 암호화(KMS) 해제(restic 이 이미 암호화 · 되돌릴 수 없다)
9b530da docs 백업 키는 버킷이 아니라 서브 계정에서(Access Key 탭 — Secret Key 는 그때만) · "이 버킷만 쓰는 하위 계정"을 "Object Storage 만 쓰는 서브 계정 · 같은 계정에 다른 버킷을 두지 않는다"로 바로잡음(README · ops/ops.env.example · 체크리스트)
3f8ba00 docs 사용자 결정 — 서버 B 의 백업 키는 메인 계정의 백업 전용 API 인증키(만드는 곳 · 기본 키와 섞지 않기 · 계정의 모든 API 를 연다는 위험), 서브 계정은 범위를 줄이는 길로 남김
5829814 docs 백업을 NCP 로 처음 확인(init · 첫 백업 14초 · 엔진 멈춤 6초 · drill 통과) — "NCP 로는 아직"을 거둠 · init 의 Stat: Access Denied 진단(S3 오류 코드 보기 — 겪은 것은 SignatureDoesNotMatch) · journalctl 은 sudo · ops.env 의 ARASYSTEM_DIR 예시 값 주의
3b59497 docs 운영 imapsync 의 큰 메일 — 크기 한도 110 MiB · 1g 에서 OOM(그동안 다른 계정 Connection refused) · docker update 2g 는 다시 만들면 사라짐 · 옮기지 못한 초안 · 메모리 예산 표의 mail 줄
6dc2777 fix(docker) mail setup — SMTP 받는 메일 크기 한도 50MiB · 리로드 뒤 EHLO SIZE 확인 · IMAP/메일 크기 한도는 [주의] 만, README 「엔진 한도」의 「받는 메일 크기」(잰 값 표 · 넘으면 · 확인하지 않은 것) · 이전 뒤 되돌리는 차례 · 문제 해결 줄, docker 스킬 한 항목
2cd2764 test(backend) jmap.client.spec.ts — 가짜 응답 없는 fetch 가 진짜 http://mail:8080 으로 나가던 것을 곧바로 연결 실패로 막고(afterEach 가 남은 주소를 잡는다), 메일 계정이 없는 세션 · 실시간 연결 403 두 테스트에 계정 정보 응답을 넣었다
349c312 test(frontend) 쓰기 화면 두 테스트(첨부 합계 · 링크로 보내기) — 대용량 파일이 ready 가 될 때까지 vi.waitFor 로 기다린 뒤 보낸다(조각 SHA-256 이 Node Web Crypto · 스레드 풀이라 부하에서 늦다), WAIT_FOR_TIMEOUT_MS
bc22f5f test(backend) CPU 시간 단언을 지금 스레드의 user 시간으로(backend/test/cpu-time.ts — process.threadCpuUsage, 없으면 예전처럼 process.cpuUsage) · 받는 사람 맞추기 한도 2.5초, .claude/rules/testing.md · nestjs 스킬 문장 — 합치지 않고 두었던 test/cpu-time-thread 의 da36e83 을 그대로 옮겨 왔다(같은 패치)

코드는 mail/scripts/setup.mjs 하나(6dc2777)와 테스트 쪽 커밋 셋(2cd2764 · 349c312 · bc22f5f — 제품 코드는 그대로). 그 밖의 공유 계약 · 백엔드 · 프론트 코드 · env 변경 없음 — 나머지는 문서(mail/README.md · ops/ops.env.example 의 주석 · .claude/rules/checklist.md 한 줄 · .claude/rules/testing.md 의 성능 단언 문장 · .claude/skills/docker/SKILL.md 한 항목 · .claude/skills/nestjs/SKILL.md 한 줄).
이 PR 을 합치면 test/cpu-time-thread 브랜치(위 da36e83 하나뿐)는 할 일이 없어져 지워도 된다.
서버 B 는 이 PR 을 합친 뒤 git pull 만 하면 된다(컨테이너를 다시 만들지 않는다 — 전환 전까지 메일 컨테이너는 docker update 2g 그대로).

확인

  • 첫 CI(run 152)의 백엔드 · 프론트 실패는 이 PR 의 변경과 상관없는 흔들리는 테스트였다 — 두 폴더의 코드는 PR #11 에서 통과한 것과 같다. 로그(자격을 붙인 API 로 읽음)로 원인을 찾아 테스트만 고쳤다.
    • 백엔드: 메일 계정이 없는 세션은 MAIL_001 이 5초 시간 초과 — 가짜 응답 없는 두 번째 요청(13단계 계정 정보 조회)이 진짜 엔진 주소로 나갔다. 고친 뒤 개발 컨테이너에서 spec 84 · 전체 3,203 통과, lint 통과.
    • 프론트: 첨부 합계는 서버 한도로 센다 에서 보내기 요청이 없었다 — 대용량 파일을 다 올리기 전에 보내기를 눌러 막혔다. 고친 뒤 TZ=UTC 전체 2,354 통과, lint · type-check 통과. digest 를 늦춘 실험으로 고치기 전에는 깨지고 고친 뒤에는 통과함을 봤다(같은 꼴의 링크로 보내기 테스트도 함께).
  • 두 번째 CI(run 153)는 프론트 · infra 통과, 백엔드만 실패 — 이것도 흔들림이었다. CPU 시간 단언 두 곳이 CI 부하에서 한도를 넘었다(contacts/address-book.spec.ts 3,230ms / 2,500 · mail/recipient-match.spec.ts 1,964ms / 1,500 — 러너가 개발 PC 라 다른 일과 CPU 를 나눈다).
    프로세스 전체 CPU 시간(GC · JIT 도우미 스레드 포함)을 재던 것을 지금 스레드의 user 시간으로 바꾼 고침(da36e83 — 2026-10-31 에 만들어 두고 합치지 않았다)을 bc22f5f 로 옮겨 왔다.
    개발 컨테이너에서 두 spec 88 통과(전체 수트를 함께 돌리는 부하에서도 세 번 모두 통과) · 전체 163 수트 3,203 통과 · lint · type-check 통과.
  • setup 변경(6dc2777)은 개발 엔진에서 두 번 돌렸다 — [변경] 받는 메일 크기 한도 … 52,428,800바이트 (50MiB) · 전 104,857,600 → [유지], 둘 다 리로드 뒤 [확인] 받는 메일 크기 한도 — EHLO SIZE 52,428,800, 종료 0.
    x:Imap 한도를 115,343,360 으로 올려 두면 [주의] 이고 값은 그대로였다. 리로드가 걸리지 않은 경우 · SIZE 가 없는 경우 등은 가짜 클라이언트로 갈래를 봤다.
    리뷰 에이전트의 지적(저장 때 줄만으로는 리로드 실패를 못 거른다 → 리로드 뒤 EHLO 확인, 첨부 합계 경계 30MB, 시험하지 않은 것 표시)을 반영했다.
  • 엔진 메모리 실측(5.5배 · 984/1024MiB · 50MiB 한도에서 581MiB · 넘으면 552)은 개발 엔진(메모리 1g · 스왑 없음)에서 cgroup anon 을 0.1초마다 읽어 쟀다.
  • 새 명령(awk · grep · doveadm -f flow who -1 · --justlogin 두 쪽 원본)은 우분투 · 데비안의 기본 awk 인 mawk 1.3.4 에서, 표본과 개발 Dovecot 2.3.21.1(mailcow 꼴 — 마스터 *)의 실제 출력으로 시험했다 — 비밀번호 · 공인 주소가 출력되지 않는다.
  • 로그 분류 · 지금 연결 두 명령은 운영 mailcow 에서 돌렸다(2026-11-03).
  • 백업 설치를 서버 B 에서 끝까지 돌렸다(2026-11-03) — NCP 버킷에 restic 저장소 → 첫 백업 [완료](final 스냅숏 · 엔진 멈춤 6초) → 타이머 둘 · 부팅 되살리기 켬 → drill [완료](계정 4 · 메일 4 · DB 표 15개 행 수 · env 가 운영과 같음). 알림(Healthchecks · UptimeRobot)은 아직.
  • 배포 차례는 서버 B 에서 그대로 돌렸다(2026-11-03) — 손 백업(DB 표 20개 · 메일 1MB) → up -d --build(마이그레이션 둘) → setup --recovery(관리자 대리 접근 뺌 · 허용 IP · 웹훅 생성 · 확인 — ZEN 시간 초과로 종료 1) → 복구 관리자 끄기 → 보통 setup [완료] 종료 0(ZEN 다시 켬).

🤖 Generated with Claude Code

## 무엇을 · 왜 PR #11(메일 이전)을 합친 뒤 운영 mailcow 를 직접 확인한 결과로 `mail/README.md` 의 imapsync 절을 바로잡고, 같은 날 서버 B 를 12 · 13단계로 올리며 겪은 것을 배포 차례에 남겼다. 그리고 이전 1 · 2차에서 나온 큰 메일 문제로 **setup 이 SMTP 받는 메일 크기 한도를 맞추게** 했다(`6dc2777` — 나머지 커밋은 문서만). - **mailcow 호스트는 사무실 PC 의 WSL**(`/opt/mailcow-dockerized` — `mailcow.conf` · 저장소가 root 소유) — 확인 · 마스터 계정 명령을 sudo 와 전체 경로로. git 의 `safe.directory` 는 건드리지 않는다. - **마스터 두 줄은 기본이 빈 값** — mailcow 의 `generate_config.sh` 가 `DOVECOT_MASTER_USER=` · `DOVECOT_MASTER_PASS=` 를 넣어 두므로 "주석이 아니면 정해 둔 것"이 아니다(운영도 둘 다 빈 값). 확인은 값 대신 비었는지만 보고, 이전을 마칠 때는 줄을 지우지 않고 값을 비운다. - **준비에 "원본 로그인만 미리"** — 대상 계정 없이 두 쪽 모두 mailcow 로 `--justlogin`. 단계 2(계정 만들기)부터는 짧게 이어야 하므로 원본 쪽 문제(마스터 · 인증서 · 앞단 길)를 그 전에 고친다. - **mailcow 는 바깥 접속을 모두 앞단 터널 주소 `10.9.0.1`(사설)로 본다** — 최근 7일 로그인 줄과 지금 연결로 확인(공인 주소 0). mailcow 의 Fail2ban 은 사설 · 루프백 주소를 막지 않으므로(원본 `netfilter/main.py`) **서버 B 를 예외에 넣을 필요가 없다**. 남는 것은 Dovecot 의 늦추기(실패한 주소의 다음 로그인 최대 15초 — 바깥 직원 모두와 같은 주소)라 실패를 되풀이하지 않는다. - **이 mailcow 의 docker 로그**(json-file · 회전 없음 · Dovecot 만 428MB)는 `--since` 만 주면 0줄이다(처음부터 읽다 중간에서 말없이 멈추는 것으로 보인다) — 로그 명령은 모두 `--tail` 로, 로그 없이 지금 연결을 보는 `doveadm who` 를 더했다. - 운영 사실: mailcow 2026-05-12 판(`32156a33`), Dovecot 2.3 줄기. - **운영 imapsync 의 큰 메일**(2026-11-03 1 · 2차 — 15계정) — 엔진 크기 한도(관리 화면 세 값 → 110 MiB, setup 은 맞추지 않는다)보다 큰 메일은 APPEND 에서 빠지고(114), 100MB 남짓 한 통은 엔진 1g 를 넘겨 OOM 으로 다시 떴다 — `docker update` 2g 뒤 다시 돌려 통과. 110 MiB 를 넘는 초안 두 통은 옮기지 못했다(사용자가 따로 보관). 이전 뒤 한도 · 메모리를 어떻게 둘지는 아래 `6dc2777` 에서 정했다. - **SMTP 받는 메일 크기 한도 50MiB**(`6dc2777` — 개발 엔진 실측 · 사용자 결정): 큰 메일 한 통에 엔진 메모리가 그 크기의 5.5배쯤 들고, SMTP 받는 한도(`x:MtaStageData` 의 `maxMessageSize`, 기본 100MiB)는 `x:Email` 한도와 따로라 기본값 · 1g 에서 바깥 104MB 한 통이 984/1024MiB 였다. 메모리는 1g 그대로 두고 setup 이 받는 한도를 50MiB 로 맞춘 뒤 리로드 뒤 EHLO 의 `SIZE` 로 확인한다(`[확인] 받는 메일 크기 한도 — EHLO SIZE …`). IMAP · 메일 크기 한도는 이전 동안 올리므로 바꾸지 않고 기본보다 크면 `[주의]` 만. 운영 전환 7번은 이전을 마치면 세 값을 관리 화면에서 기본으로 → setup 의 `[확인]` → 메모리 1g 차례다 — **이 PR 을 합친 뒤 서버 B 에서 `git pull` 해 둔다**. | 커밋 | 내용 | | --- | --- | | `e664217` docs | mailcow 호스트는 사무실 PC 의 WSL — 확인 · 마스터 계정 명령을 sudo 로 | | `34d63cf` docs | 마스터 두 줄은 기본이 빈 값(확인 명령 바로잡기 · 끝낼 때 값 비우기) · 준비의 "원본 로그인만" · mailcow 가 보는 접속 주소 | | `b43b747` docs | 운영 확인 결과 — 바깥 접속은 모두 `10.9.0.1` 이라 Fail2ban 예외 필요 없음 · 로그는 `--tail` · `doveadm who` | | `07b7fcc` docs | 서버 B 12 · 13단계 배포(2026-11-03)에서 겪은 것 — DB digest 가 같으면 날짜를 기다리지 않음 · 12단계 전 서버는 웹훅 키 먼저 · `--recovery` 의 Spamhaus ZEN 시간 초과(종료 1)는 보통 setup 이 다시 켬 · 마이그레이션은 `migrations` 표로 · 손 백업에 DB 덤프(떠 있는 `alpine` 대신 지금 도는 DB 이미지) | | `0388cf6` docs | restic 비밀번호는 비밀번호 관리자에서 먼저 만들어 화면 · 셸 기록 없이 서버에 넣기(설치 2 · `ops/ops.env.example` 머리말) — 서버 밖 사본이 백업보다 먼저 생긴다 | | `feabe0d` docs | 백업 버킷 만들기 — 공개 안 함 · 잠금 해제(보존 기간은 prune 을 막는다) · 암호화(KMS) 해제(restic 이 이미 암호화 · 되돌릴 수 없다) | | `9b530da` docs | 백업 키는 버킷이 아니라 서브 계정에서(Access Key 탭 — Secret Key 는 그때만) · "이 버킷만 쓰는 하위 계정"을 "Object Storage 만 쓰는 서브 계정 · 같은 계정에 다른 버킷을 두지 않는다"로 바로잡음(README · `ops/ops.env.example` · 체크리스트) | | `3f8ba00` docs | 사용자 결정 — 서버 B 의 백업 키는 메인 계정의 백업 전용 API 인증키(만드는 곳 · 기본 키와 섞지 않기 · 계정의 모든 API 를 연다는 위험), 서브 계정은 범위를 줄이는 길로 남김 | | `5829814` docs | 백업을 NCP 로 처음 확인(init · 첫 백업 14초 · 엔진 멈춤 6초 · drill 통과) — "NCP 로는 아직"을 거둠 · `init` 의 `Stat: Access Denied` 진단(S3 오류 코드 보기 — 겪은 것은 `SignatureDoesNotMatch`) · `journalctl` 은 sudo · ops.env 의 `ARASYSTEM_DIR` 예시 값 주의 | | `3b59497` docs | 운영 imapsync 의 큰 메일 — 크기 한도 110 MiB · 1g 에서 OOM(그동안 다른 계정 `Connection refused`) · `docker update` 2g 는 다시 만들면 사라짐 · 옮기지 못한 초안 · 메모리 예산 표의 mail 줄 | | `6dc2777` fix(docker) | mail setup — SMTP 받는 메일 크기 한도 50MiB · 리로드 뒤 EHLO SIZE 확인 · IMAP/메일 크기 한도는 `[주의]` 만, README 「엔진 한도」의 「받는 메일 크기」(잰 값 표 · 넘으면 · 확인하지 않은 것) · 이전 뒤 되돌리는 차례 · 문제 해결 줄, docker 스킬 한 항목 | | `2cd2764` test(backend) | `jmap.client.spec.ts` — 가짜 응답 없는 fetch 가 진짜 `http://mail:8080` 으로 나가던 것을 곧바로 연결 실패로 막고(afterEach 가 남은 주소를 잡는다), 메일 계정이 없는 세션 · 실시간 연결 403 두 테스트에 계정 정보 응답을 넣었다 | | `349c312` test(frontend) | 쓰기 화면 두 테스트(첨부 합계 · 링크로 보내기) — 대용량 파일이 ready 가 될 때까지 `vi.waitFor` 로 기다린 뒤 보낸다(조각 SHA-256 이 Node Web Crypto · 스레드 풀이라 부하에서 늦다), `WAIT_FOR_TIMEOUT_MS` | | `bc22f5f` test(backend) | CPU 시간 단언을 지금 스레드의 user 시간으로(`backend/test/cpu-time.ts` — `process.threadCpuUsage`, 없으면 예전처럼 `process.cpuUsage`) · 받는 사람 맞추기 한도 2.5초, `.claude/rules/testing.md` · nestjs 스킬 문장 — 합치지 않고 두었던 `test/cpu-time-thread` 의 `da36e83` 을 그대로 옮겨 왔다(같은 패치) | 코드는 `mail/scripts/setup.mjs` 하나(`6dc2777`)와 테스트 쪽 커밋 셋(`2cd2764` · `349c312` · `bc22f5f` — 제품 코드는 그대로). 그 밖의 공유 계약 · 백엔드 · 프론트 코드 · env 변경 없음 — 나머지는 문서(`mail/README.md` · `ops/ops.env.example` 의 주석 · `.claude/rules/checklist.md` 한 줄 · `.claude/rules/testing.md` 의 성능 단언 문장 · `.claude/skills/docker/SKILL.md` 한 항목 · `.claude/skills/nestjs/SKILL.md` 한 줄). 이 PR 을 합치면 `test/cpu-time-thread` 브랜치(위 `da36e83` 하나뿐)는 할 일이 없어져 지워도 된다. 서버 B 는 이 PR 을 합친 뒤 `git pull` 만 하면 된다(컨테이너를 다시 만들지 않는다 — 전환 전까지 메일 컨테이너는 `docker update` 2g 그대로). ## 확인 - **첫 CI(run 152)의 백엔드 · 프론트 실패는 이 PR 의 변경과 상관없는 흔들리는 테스트였다** — 두 폴더의 코드는 PR #11 에서 통과한 것과 같다. 로그(자격을 붙인 API 로 읽음)로 원인을 찾아 테스트만 고쳤다. - 백엔드: `메일 계정이 없는 세션은 MAIL_001` 이 5초 시간 초과 — 가짜 응답 없는 두 번째 요청(13단계 계정 정보 조회)이 진짜 엔진 주소로 나갔다. 고친 뒤 개발 컨테이너에서 spec 84 · 전체 3,203 통과, lint 통과. - 프론트: `첨부 합계는 서버 한도로 센다` 에서 보내기 요청이 없었다 — 대용량 파일을 다 올리기 전에 보내기를 눌러 막혔다. 고친 뒤 `TZ=UTC` 전체 2,354 통과, lint · type-check 통과. digest 를 늦춘 실험으로 고치기 전에는 깨지고 고친 뒤에는 통과함을 봤다(같은 꼴의 링크로 보내기 테스트도 함께). - **두 번째 CI(run 153)는 프론트 · infra 통과, 백엔드만 실패 — 이것도 흔들림이었다.** CPU 시간 단언 두 곳이 CI 부하에서 한도를 넘었다(`contacts/address-book.spec.ts` 3,230ms / 2,500 · `mail/recipient-match.spec.ts` 1,964ms / 1,500 — 러너가 개발 PC 라 다른 일과 CPU 를 나눈다). 프로세스 전체 CPU 시간(GC · JIT 도우미 스레드 포함)을 재던 것을 지금 스레드의 user 시간으로 바꾼 고침(`da36e83` — 2026-10-31 에 만들어 두고 합치지 않았다)을 `bc22f5f` 로 옮겨 왔다. 개발 컨테이너에서 두 spec 88 통과(전체 수트를 함께 돌리는 부하에서도 세 번 모두 통과) · 전체 163 수트 3,203 통과 · lint · type-check 통과. - setup 변경(`6dc2777`)은 개발 엔진에서 두 번 돌렸다 — `[변경] 받는 메일 크기 한도 … 52,428,800바이트 (50MiB) · 전 104,857,600` → `[유지]`, 둘 다 리로드 뒤 `[확인] 받는 메일 크기 한도 — EHLO SIZE 52,428,800`, 종료 0. `x:Imap` 한도를 115,343,360 으로 올려 두면 `[주의]` 이고 값은 그대로였다. 리로드가 걸리지 않은 경우 · SIZE 가 없는 경우 등은 가짜 클라이언트로 갈래를 봤다. 리뷰 에이전트의 지적(저장 때 줄만으로는 리로드 실패를 못 거른다 → 리로드 뒤 EHLO 확인, 첨부 합계 경계 30MB, 시험하지 않은 것 표시)을 반영했다. - 엔진 메모리 실측(5.5배 · 984/1024MiB · 50MiB 한도에서 581MiB · 넘으면 552)은 개발 엔진(메모리 1g · 스왑 없음)에서 cgroup anon 을 0.1초마다 읽어 쟀다. - 새 명령(awk · grep · `doveadm -f flow who -1` · `--justlogin` 두 쪽 원본)은 우분투 · 데비안의 기본 awk 인 mawk 1.3.4 에서, 표본과 개발 Dovecot 2.3.21.1(mailcow 꼴 — 마스터 `*`)의 실제 출력으로 시험했다 — 비밀번호 · 공인 주소가 출력되지 않는다. - 로그 분류 · 지금 연결 두 명령은 운영 mailcow 에서 돌렸다(2026-11-03). - 백업 설치를 서버 B 에서 끝까지 돌렸다(2026-11-03) — NCP 버킷에 restic 저장소 → 첫 백업 `[완료]`(final 스냅숏 · 엔진 멈춤 6초) → 타이머 둘 · 부팅 되살리기 켬 → `drill` `[완료]`(계정 4 · 메일 4 · DB 표 15개 행 수 · env 가 운영과 같음). 알림(Healthchecks · UptimeRobot)은 아직. - 배포 차례는 서버 B 에서 그대로 돌렸다(2026-11-03) — 손 백업(DB 표 20개 · 메일 1MB) → `up -d --build`(마이그레이션 둘) → `setup --recovery`(관리자 대리 접근 뺌 · 허용 IP · 웹훅 생성 · 확인 — ZEN 시간 초과로 종료 1) → 복구 관리자 끄기 → 보통 setup `[완료]` 종료 0(ZEN 다시 켬). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ttipo added 11 commits 2026-11-03 21:41:31 +09:00
mailcow.conf 와 mailcow 저장소가 root 소유라 sudo 없이는 grep 이 거절되고 git 이 dubious ownership 으로 멈췄다(2026-11-02 사용자 실행).
이전하는 동안 그 PC 가 절전 · 재부팅되면 imapsync 가 끊긴다는 줄도 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 mailcow.conf 에 DOVECOT_MASTER_USER · _PASS 가 주석 없이 있었는데, mailcow 의
generate_config.sh 가 두 줄을 빈 값으로 넣어 두므로 "주석이 아니면 정해 둔 것"으로 읽으면 틀린다.
값 대신 비었는지만 보는 확인으로 바꾸고, 끝낼 때는 줄을 지우지 않고 값을 비우게 했다.

mailcow 의 Fail2ban(netfilter)은 사설 · 루프백 주소를 막지 않는다(원본 main.py) — 예외가
필요한지는 mailcow 가 Contabo 앞단 · WSL 을 지난 서버 B 를 어떤 주소로 보느냐에 달렸다.
준비에 계정 없이 원본 로그인만 보는 --justlogin(두 쪽 모두 mailcow)과 mailcow 로그에서
그 주소 · 지난 사흘의 접속 주소를 보는 명령을 넣었다(개발 Dovecot 2.3.21.1 · mawk 1.3.4 로 시험).
운영 사실: 설치 폴더 /opt/mailcow-dockerized, 판 2026-05-12(32156a33).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 mailcow 에서 확인(2026-11-03): 마스터 두 값은 빈 값(아직 아무도 고정하지 않음).
최근 7일 Dovecot 로그인 줄과 지금 연결이 모두 앞단 터널 주소 10.9.0.1(사설) — 공인 주소 0.
mailcow 의 Fail2ban 은 사설 주소를 막지 않으므로 서버 B 를 예외에 넣지 않는다.
남는 것은 Dovecot 의 늦추기(같은 주소의 바깥 직원 모두)라 실패를 되풀이하지 않는다.

이 mailcow 의 docker 로그(json-file · 회전 없음 · 428MB)는 --since 만 주면 0줄이고
--tail 은 된다 — 처음부터 읽다 중간의 읽지 못하는 줄에서 말없이 멈추는 것으로 보인다.
로그 명령을 --tail 로 바꾸고, 로그 없이 지금 연결을 보는 doveadm who 를 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
설치 2)는 서버에서 openssl 로 만들고 "비밀번호 관리자에도 둔다"고만 적어, 서버 밖에 두는
길이 빠져 있었다. 생성기로 먼저 만들어 저장한 뒤 read -rsp 로 붙여 넣게 바꿨다 — 서버 밖
사본이 백업보다 먼저 생긴다. 서버에서 만드는 길은 sudo cat 으로 한 번 옮기는 줄과 함께 남겼다.
회사가 꺼낼 수 있는 곳에 두라는 것(운영자 한 사람만이 아니라)과 재설정이 없다는 것을 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NCP 버킷 생성 화면의 칸마다 무엇을 고르는지 적는다. 공개는 목록을 누구에게나 열고,
잠금(보존 기간)은 restic 의 prune 을 막고, KMS 암호화는 되돌릴 수 없고 restic 이 이미 암호화한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NCP 버킷에는 따로 키가 없다(사용자가 버킷을 만든 뒤 키를 찾지 못했다). 키는 서브 계정의
Access Key 탭에서 만들고 Secret Key 는 그때만 보인다 — 차례를 README 에 적었다.
"이 버킷만 쓰는 하위 계정"은 NCP 문서로 뒷받침되지 않는다(관리형 정책은 Object Storage 전체,
사용자 정의 정책에도 버킷을 고르는 칸이 문서에 없다) — README · ops.env 예시 · 체크리스트를
"Object Storage 만 쓰는 서브 계정 · 같은 계정에 다른 버킷을 두지 않는다"로 바로잡았다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
사용자가 서브 계정을 만들지 않고 메인 계정의 API 인증키로 가기로 했다(2026-11-03).
README · ops.env 예시에 만드는 곳(My Account › 계정 및 보안 관리 › 보안 관리 › 접근 관리 —
계정마다 2개, 기본 키와 섞지 않게 새로)과, 그 키가 계정의 모든 API 를 연다는 위험을 함께 적었다.
서브 계정 차례는 범위를 줄이는 길로 남겼다. 체크리스트(템플릿 공용)의 권장은 그대로 둔다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 서버 B 에서 restic 저장소를 NCP Object Storage 에 만들고 첫 백업(14초 · 메일 엔진
멈춤 6초) · 복원 연습(계정 · 메일 · DB 행 수 · env 가 운영과 같음)을 마쳤다 — "NCP 로는 아직"을 거둔다.
init 의 "Stat: Access Denied" 는 HEAD 라 까닭이 안 보여, 같은 설정으로 목록을 조회해 S3 오류 코드를
보는 명령을 더했다(겪은 것은 SignatureDoesNotMatch — 콘솔 복사 버튼으로 다시 넣어 풀림).
journalctl 은 sudo 로, ops.env 의 ARASYSTEM_DIR 예시 값을 그대로 두지 않는다는 주석도 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 운영 1 · 2차에서 겪은 것을 imapsync 절에 남긴다. 한도보다 큰 메일은
APPEND 에서 빠지고(114), 100MB 남짓 한 통은 엔진 1g 를 넘겨 OOM 으로 다시 떴다
(그동안 다른 계정은 Connection refused). docker update 2g 는 컨테이너를 다시
만들면 사라지고, 한도를 올린 채 두면 메모리 예산의 합이 바뀐다 — 이전 뒤
어떻게 둘지는 아직 정하지 않았다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(docker): mail setup — SMTP 받는 메일 크기 한도 50MiB · 리로드 뒤 EHLO 로 확인 · IMAP/메일 크기 한도는 [주의] 만
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Failing after 8m39s
CI / Frontend (lint · type-check · test · build) (pull_request) Failing after 8m5s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m56s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m32s
6dc2777834
개발 엔진 실측(2026-11-03, 0.16.27): 큰 메일 한 통을 받으면 엔진 메모리가 그 크기의
5.5배쯤 는다(IMAP APPEND · SMTP 받기 같다). SMTP 받는 한도(x:MtaStageData 의
maxMessageSize, 기본 100MiB)는 x:Email 한도와 따로라, 기본값 · 1g 에서 바깥 104MB
한 통이 984/1024MiB 였다(운영은 106.4MB IMAP APPEND 로 OOM).

사용자 결정 — 메일 컨테이너 메모리는 1g 그대로 두고, setup 이 받는 한도를 50MiB 로
맞춘 뒤 리로드 뒤 EHLO 의 SIZE 로 실제로 걸렸는지 본다(저장 때 줄만으로는 리로드
실패를 못 거른다 — 리뷰). IMAP · 메일 크기 한도는 imapsync 이전 동안 운영자가 잠시
올리므로 setup 이 바꾸지 않고 기본보다 크면 [주의] 만 낸다.

README 에 잰 값 · 정책 · 이전 뒤 되돌리는 차례(세 값 → setup [확인] → 1g)와
문제 해결 줄, docker 스킬에 한 항목.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ttipo added 2 commits 2026-11-03 23:31:47 +09:00
이 spec 은 fetch 를 spyOn 으로 감싸기만 해, 가짜 응답을 넣지 않은 호출이 진짜
http://mail:8080 으로 나갔다. 13단계부터 메일 계정이 없는 세션은 계정 정보를 한 번
더 묻는데, 그 요청이 CI 컨테이너에서 이름 풀기를 기다리다 테스트 5초를 넘겼다
(2026-11-03 CI — 개발 컨테이너에서는 개발 엔진이 실제로 받았다).

- 가짜 응답이 없는 fetch 는 곧바로 연결 실패로 거절하고 주소를 남긴다 — afterEach 가
  남은 주소가 있으면 그 테스트를 실패시킨다. jest-mock 30 의 mockReset 은 구현까지
  지우므로 resetFetchMock 이 기본을 다시 건다.
- 메일 계정이 없는 세션 · 실시간 연결 403 두 테스트에 계정 정보 응답을 넣고 요청을 단언.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(frontend): 대용량 파일을 다 올린 뒤 보낸다 — 조각 SHA-256 이 스레드 풀이라 부하에서 늦다
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Failing after 10m31s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m18s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 14m21s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m30s
349c312989
쓰기 화면의 두 테스트는 대용량 파일을 고른 뒤 양보(flushPromises) 두 번 만에 다 올렸다고
보았다. 조각의 SHA-256 은 jsdom 이 아니라 Node 의 Web Crypto(libuv 스레드 풀)라, CI 부하에서
그 안에 끝나지 않았다. 다 올리기 전에 누른 [보내기]는 막혀 보내기 요청이 끝내 나가지 않아
payload 가 undefined 였다(2026-11-03 CI — ComposePage.spec.ts 첨부 합계 테스트).

- 두 테스트(첨부 합계 · 링크로 보내기)는 대용량 줄이 ready 가 될 때까지 vi.waitFor 로 본다.
  첨부 합계 테스트는 보내기 전에 막는 까닭이 없음도 단언한다.
- test-utils/timeouts.ts 에 WAIT_FOR_TIMEOUT_MS(15초 — 테스트 한도 20초보다 짧게)를 둔다.
- digest 를 늦춘 실험으로 고치기 전에는 깨지고 고친 뒤에는 통과함을 확인했다(실험은 되돌림).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ttipo added 1 commit 2026-11-04 00:16:41 +09:00
test(backend): CPU 시간 단언을 지금 스레드의 user 시간으로 — CI 흔들림 고침
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m21s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 12m37s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m18s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m10s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 14m41s
CI / Frontend (lint · type-check · test · build) (push) Successful in 14m29s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 2m26s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 12m27s
bc22f5f3ba
문서만 바꾼 PR(#8, run 138)의 CI 에서 할당이 많은 성능 단언 셋이 넘쳤다(받는 사람 맞추기 3.29초 · JSON 모양 1.35초 · DTO 2.94초).
프로세스 user + system 시간에는 V8 의 GC · JIT 도우미 스레드와 페이지 폴트 몫이 들어가 기계 사정을 따라 크게 흔들린다.
같은 Linux 컨테이너에서 전체 테스트 16회: 프로세스 user + system 은 4회 넘침, 스레드 user 는 0회.
막으려는 옛 경로(제곱)는 거의 user 시간이라 그대로 잡힌다(받는 사람 맞추기 옛 방법 51~70초).
받는 사람 맞추기 한도는 CI 쪽 값(0.17~0.25초)의 10배쯤인 2.5초로 올렸다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ttipo merged commit bc22f5f3ba into main 2026-11-04 07:59:25 +09:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ttipo/arasystem#12