diff --git a/.claude/skills/docker/SKILL.md b/.claude/skills/docker/SKILL.md index 3684632..175d86d 100644 --- a/.claude/skills/docker/SKILL.md +++ b/.claude/skills/docker/SKILL.md @@ -484,7 +484,7 @@ node --env-file=.env.development mail/scripts/setup.mjs - **백업**(restic → Object Storage, 보존 일 7 · 주 4 · 월 6, 일요일 prune · check 5%): 메일 볼륨은 켜 둔 채 미리 올리고(warm — restic 종료 3 은 받아들임) → 엔진을 잠깐 멈춰(9초 안팎) 바뀐 것만(final — 상한 `BACKUP_FINAL_TIMEOUT` 15분) → 다시 띄운다. DB `pg_dump -Fc`(+ `pg_restore -l` 확인) · 업로드 볼륨 · env 둘. 어떻게 끝나도 엔진을 다시 띄운다(EXIT trap + `ExecStopPost`, 멈추는 중 신호면 멈춤을 끝낸 뒤). 비밀 사본(env · 덤프)은 끝날 때 지운다. **매일 03:30 에 웹메일이 10초 안팎 `MAIL_001`** 이다(엔진 멈춤). 백업과 복원 연습은 같은 잠금 — 잠긴 동안 손으로 부른 엔진 띄우기는 건너뛴다(final 도중 띄우면 일관되지 않은 스냅숏이 final 이 된다). -- **복원**(`arasystem-restore.sh` — snapshots · check · drill · files · volumes · db): final 태그 스냅숏만, 쓰는 컨테이너가 있거나 볼륨이 비지 않았으면 멈춘다(운영 볼륨을 덮지 않게), 새 서버는 `.env.production` 없이 ops.env 의 `PROJECT_NAME` 으로 files 부터. 분기마다 `drill`. +- **복원**(`arasystem-restore.sh` — snapshots · check · drill · files · volumes · db): final 태그 스냅숏만, 쓰는 컨테이너가 있거나 볼륨이 비지 않았으면 멈춘다(운영 볼륨을 덮지 않게), 새 서버는 `.env.production` 없이 ops.env 의 `PROJECT_NAME` 으로 files 부터. 한 달에 한 번 `drill`(mail/README "백업 › 복원" 이 기준). - **15분 점검**: 디스크 두 곳 · 컨테이너 health · 백엔드 health · 메일 대기열 · 인증서 셋 · 백업 나이(26시간 이상) · ClamAV 서명 · Spamhaus 키 → Healthchecks.io(정상 · 실패 + 까닭 한 줄). 바깥은 UptimeRobot(포털 HTTPS · mx1:993 · mx1:25) — 알림은 우리 메일 서버가 아닌 주소로. - **비-root 예외**: restic 보조 컨테이너는 root 로 돌되 `--cap-drop ALL` + 필요한 권한만(읽기 `DAC_READ_SEARCH`, 복원은 `DAC_OVERRIDE` · `CHOWN` · `FOWNER`) · `no-new-privileges` ([[security]]). 연습 엔진(Stalwart)을 `no-new-privileges` 로 띄우려면 `NET_BIND_SERVICE` 가 든다. - 비밀은 명령줄에 싣지 않는다 — `docker exec -e 이름` · 표준 입력 · 읽기 전용 마운트, Healthchecks 주소도 `curl -K -`. @@ -568,6 +568,9 @@ node --env-file=.env.development mail/scripts/setup.mjs `nest-cli.json` 의 `deleteOutDir` 는 마운트 지점을 지우려다 EBUSY 로 죽으므로, dev `command` 는 기동 때 볼륨 내용만 비우고 `deleteOutDir` 만 끈 설정 사본으로 `start:dev` 를 돌린다 (`-V` 로 재생성하면 이미지의 산출물로 다시 채워진다). - **dev backend 메모리 한도는 2g** — watch 컴파일과 타입 검사가 함께 돌아 base 의 1g 에서 OOM 으로 죽었다 (운영은 1g 그대로). +- **db 메모리 한도는 1g · 공유 버퍼(`shared_buffers`) 256MB** (14단계 — 메일 검색 색인의 찾기는 표 · 색인이 메모리에 있어야 1초 안이다, 캐시가 비면 3~8배). + 공유 버퍼는 compose 의 db `command`(`postgres -c …` — 명령줄 값이 데이터 폴더의 설정보다 이긴다)이고 개발도 같은 값이다(dev 오버라이드가 덮어쓰지 않는다). + 8GB 서버의 한도 합 · 운영에 올리는 차례는 mail/README.md "메모리 예산". - **dev backend 는 컨테이너를 기동할 때 한 번 `migration:run` 을 한다** (`dc down -v` 등으로 DB 를 새로 만든 뒤에도 바로 뜨게). watch 재시작은 이 단계를 다시 돌지 않는다 — 그래서 생성만 하고 검토 전인 마이그레이션이 파일을 저장할 때마다 적용되지는 않는다. ⚠️ 다만 **컨테이너를 다시 띄우면** 그때 있는 마이그레이션이 적용된다. 마이그레이션을 고치는 중(revert → 수정 → run)에는 diff --git a/docker-compose.yml b/docker-compose.yml index 209a2ea..38871d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -234,6 +234,10 @@ services: # 판 · digest 까지 고정한다(루트 env 의 DB_IMAGE — docker 스킬 "이미지 버전 고정"). 메이저(15)는 데이터 형식이라 그대로 둔다 — # 올리려면 pg_dump → 새 메이저에 복원(mail/README.md "백업"의 복원 차례). 아래 watchtower 표시는 13단계에 뺐다(판을 사람이 올린다) image: ${DB_IMAGE} + # 공유 버퍼(shared_buffers) 256MB — 기본 128MB 의 두 배 · 메모리 한도(아래 deploy — 1g)의 1/4. 14단계 메일 검색 색인 때문이다(아래 deploy 의 "메모리"). + # 공식 이미지의 꼴이다 — entrypoint(docker-entrypoint.sh)가 처음 기동의 initdb 를 하고 이 인자를 postgres 에 그대로 넘긴다. + # 명령줄 값은 데이터 폴더의 postgresql.conf · ALTER SYSTEM 보다 이긴다 — 값은 이 한 곳에 둔다(개발도 같다 — docker-compose.dev.yml 은 덮어쓰지 않는다) + command: ["postgres", "-c", "shared_buffers=256MB"] expose: - "5432" environment: @@ -252,11 +256,15 @@ services: timeout: 5s retries: 5 start_period: 10s + # 메모리 — 1g (14단계에 512m 에서 올렸다 — 2026-11-05 사용자 결정). 메일 검색 색인의 찾기는 표 · 색인이 메모리에 있을 때만 1초 안이다 — + # 2026-11-05 개발(1만 통 · 색인 121MB)에서 캐시가 비면 3~8배 느렸다. 512m 로도 모자라지는 않았다(채우기 0.16GB 안팎 · 찾기 시험 최대 236MiB) — 늘린 몫은 색인을 메모리에 두는 자리다. + # 공유 버퍼(위 command) 밖은 연결마다의 작업 메모리와 표 · 색인의 페이지 캐시다 — 페이지 캐시도 이 한도에 잡힌다(다시 쓸 수 있는 메모리라 줄여 가며 쓴다). + # 8GB 서버의 합은 mail/README.md "메모리 예산". 값을 바꾸면 다음 up 이 db 를 다시 만든다 — 수 초 멈추고 backend 는 다시 붙는다(같은 절의 차례) deploy: resources: limits: cpus: '0.5' - memory: 512m + memory: 1g # ---------------------------------------- # 4. Redis — 내부 네트워크 전용 · 기본 비활성 diff --git a/mail/README.md b/mail/README.md index f6b241d..2cbfdac 100644 --- a/mail/README.md +++ b/mail/README.md @@ -826,10 +826,15 @@ sudo docker exec "${PROJECT_NAME}-backend" node -e "const s=require('net').conne | clamav-updater (freshclam) | 1g | 수십 MB | 처음 기동(서명 전체를 받아 읽어 봄) 0.7GB, 평소 갱신은 daily 만 | | mail | 1g | 0.1~0.3GB (큰 메일 뒤 0.4GB 남짓까지 쌓인다 — 2026-11-03) | 메일 한 통을 받을 때 그 크기 × 5.5 쯤 더 — 받는 한도 50MiB(setup)면 +0.3GB 남짓. 운영에서 106.4MB 한 통이 1g 를 넘었다(2026-11-03 — 개발 104MB 는 984/1024MiB · 서버 B 는 이전하는 동안만 2g, [큰 메일](#큰-메일-운영-2026-11-03) · [받는 메일 크기](#엔진-한도)) | | backend | 1g | 0.3~0.5GB | 조각 · 받기는 스트림이라 파일 크기와 상관없다 | -| db · frontend · mail-resolver | 512m · 512m · 128m | 작다 | | -| **합** | **6.15GB** | 2.5~3GB | 나머지 1.8GB 남짓을 커널 · dockerd · 페이지 캐시가 쓴다 | -| restic (백업 — 13단계, compose 밖) | 1g | 돌 때만 — 매일 03:30 몇 분 | 0.63GB (데이터 1.2GB — 2026-10-31 MinIO). 그때는 합이 7.15GB — 사용이 적은 새벽이라 둔다. 복원 연습은 엔진 1g · DB 512m 를 더 띄운다(한 달에 한 번 — 낮에 하지 않는다) | +| db | 1g (14단계에 512m 에서) | 0.16GB 안팎(색인을 채우는 동안도) · 찾기 시험 동안 최대 0.24GB (2026-11-05 · 그때는 공유 버퍼 128MB · 한도 512m) | 공유 버퍼(`shared_buffers` 256MB — compose 의 `command`)가 차고 그 위로 표 · 색인의 페이지 캐시(한도 안에서 줄여 쓴다). 메일 검색은 이것이 메모리에 있을 때만 1초 안이다 — 캐시가 비면 3~8배(1만 통 · 색인 121MB) | +| frontend · mail-resolver | 512m · 128m | 작다 | | +| **합** | **6.65GB** | 2.5~3GB (14단계 전 — db 는 공유 버퍼가 찬 만큼 는다) | 나머지 1.3GB 남짓을 커널 · dockerd · 페이지 캐시가 쓴다 | +| restic (백업 — 13단계, compose 밖) | 1g | 돌 때만 — 매일 03:30 몇 분 | 0.63GB (데이터 1.2GB — 2026-10-31 MinIO). 그때는 합이 7.65GB — 사용이 적은 새벽이라 둔다. 복원 연습은 엔진 1g · DB 512m(스크립트에 고정 — compose 의 db 한도를 따르지 않는다)를 더 띄워 합이 8.15GB 로 8GB 를 넘는다(14단계부터 — 그 전은 7.65GB. 한 달에 한 번 — 낮에 하지 않는다) | +- **db 1g · 공유 버퍼 256MB 를 운영에 올릴 때**(14단계 배포와 함께 — 한 번): 다음 `up` 이 db 를 다시 만든다 — 수 초 멈추고(데이터는 `DB_DATA_DIR` 그대로) 떠 있는 backend 는 다시 붙는다(그동안의 요청은 실패). + 퇴근 뒤 · 백업(03:30) · 복원 연습을 피해 `git pull` → `sudo docker compose --env-file .env.production up -d db`(db 만 먼저) → `sudo docker stats --no-stream "${PROJECT_NAME}-db"` 의 한도 `1GiB` · + `sudo docker exec "${PROJECT_NAME}-db" sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "show shared_buffers"'` 가 `256MB` → 14단계의 `up -d --build`. + mail 이 이전 때의 2g(`docker update`)로 남아 있으면 먼저 되돌린다([큰 메일](#큰-메일-운영-2026-11-03) 3. — 그대로면 합이 7.65GB · 백업 때 8.65GB). - **ClamAV 의 `ConcurrentDatabaseReload` 를 켜지 않는다**(기본은 켬) — 켜면 서명을 다시 읽는 동안(하루 몇 번) 두 벌(2GB 남짓)을 들어 2g 한도에서 죽는다. 한도를 올려 켜려면 합이 8GB 를 넘지 않는지 다시 본다. - 서명은 해마다 는다 — `docker stats` 의 clamav 가 한도의 80%(1.6GB)를 넘으면 한도를 올리거나(합을 다시 본다) 서버를 키운다. @@ -1722,7 +1727,7 @@ sudo rm -f /root/imapsync/mailcow-master.pass /root/imapsync/migration.pass `[변경]` · `[유지]` 줄은 저장한 값이고, 엔진에 걸린 값은 리로드 뒤의 이 `[확인]` 줄이다(리로드가 실패하면 엔진은 옛 한도로 받는다). 3. 메모리를 compose 의 값으로 — `sudo docker update --memory 1g --memory-swap 2g -mail`. - 받는 한도를 낮추기 전에 1g 로 돌리면 바깥 큰 메일 한 통에 엔진이 죽을 수 있다([엔진 한도](#엔진-한도) "받는 메일 크기"). 올린 채 두면 엔진에 2g 가 들고, [메모리 예산](#메모리-예산)의 합이 7.15GB, 백업이 도는 동안은 8.15GB 로 8GB 를 넘는다. + 받는 한도를 낮추기 전에 1g 로 돌리면 바깥 큰 메일 한 통에 엔진이 죽을 수 있다([엔진 한도](#엔진-한도) "받는 메일 크기"). 올린 채 두면 엔진에 2g 가 들고, [메모리 예산](#메모리-예산)의 합이 7.65GB, 백업이 도는 동안은 8.65GB 로 8GB 를 넘는다(14단계의 db 1g 부터 — 그 전은 7.15 · 8.15GB). #### 개발에서 확인한 것 (imapsync — 2026-11-02)