이 글의 위치: 기술 블로그 → 인프라 · 「쿠버네티스 SMTP」 시리즈의 사전 정리편.
작성·검증 방식은 Editorial Policy 를 참고하세요.
이 글의 모든 프로세스 트리·메모리 수치는 제 로컬 도커(Debian 12, 커널 6.12.76-linuxkit)에서 직접 띄워 잰 값입니다.
메일 서버 이름이 너무 많아서 막혔던 지점
쿠버네티스에 SMTP를 직접 올려 보기로 하고 자료를 찾기 시작했는데, 첫 관문이 기술이 아니라 이름이었습니다. Postfix, Exim, OpenSMTPD, Haraka, Dovecot, Rspamd, Mailu, Mailcow, Docker Mailserver, Stalwart, Postal, Mailpit… 검색 결과는 이것들을 한 줄에 나열하지만, 정작 "그래서 뭘 골라야 하나"에는 답하지 않습니다.
며칠 헤매고 나서 알게 된 건, 이 이름들이 애초에 같은 층에 있지 않다는 것이었습니다. Postfix와 Mailcow를 비교하는 건 "Nginx와 Ubuntu 중 뭐가 낫나"를 묻는 것과 비슷합니다. 하나는 부품이고 하나는 그 부품을 포함한 묶음입니다.
그래서 이 글은 순위표를 만들지 않습니다. 대신 층을 가르고, 각 층의 대표들이 리눅스에서 실제로 어떤 모양으로 도는지 직접 띄워서 보고, 그 모양이 쿠버네티스에서 어떤 제약으로 바뀌는지까지 이어 보려고 합니다.
1. 먼저 층을 가른다
메일 시스템은 한 덩어리가 아니라 역할이 뚜렷하게 갈린 부품들의 조립입니다.
| 층 | 하는 일 | 대표 소프트웨어 |
|---|---|---|
| MTA (Mail Transfer Agent) | 서버 간 메일 전달. SMTP를 말한다 | Postfix, Exim, OpenSMTPD, Haraka |
| MDA / 저장·조회 | 최종 배달, 메일함 보관, IMAP·POP3로 조회 | Dovecot, Cyrus |
| 필터 | 스팸·바이러스 검사, DKIM 서명 | Rspamd, SpamAssassin, ClamAV, OpenDKIM |
| 인증·디렉터리 | 계정·도메인·비밀번호 | SQL, LDAP, 자체 관리 UI |
| 배포판(묶음) | 위 전부를 한 세트로 묶어 배포 | Mailu, Mailcow, Docker Mailserver |
| 발송 전용 플랫폼 | 앱이 보내는 메일 + 추적·API·웹훅 | Postal, Stalwart(일부), 상용 SendGrid |
| 개발용 캡처 | 실제로 보내지 않고 가로채 보여줌 | Mailpit, MailHog |
이 표만 손에 쥐면 앞의 혼란은 거의 정리됩니다. Postfix·OpenSMTPD·Haraka는 서로 대체 가능한 MTA이고, Mailu·Mailcow는 그 MTA에 Dovecot·Rspamd·웹UI를 얹어 놓은 세트이며, Postal은 사람의 메일함이 아니라 애플리케이션의 발송을 다루는 다른 목적의 물건입니다.
한 줄 정의
| 프로젝트 | 한 줄 정의 | 층 |
|---|---|---|
| Postfix | Sendmail을 대체하려고 안전성·명료함을 목표로 다시 쓴, 사실상의 표준 MTA | MTA |
| Exim | 설정 언어 하나로 라우팅 규칙을 세밀하게 짜는 단일 바이너리 MTA | MTA |
| OpenSMTPD | OpenBSD 진영이 "설정 파일이 읽히는 MTA"를 목표로 만든 경량 MTA | MTA |
| Haraka | Node.js로 짜인 플러그인 우선 MTA — 수신 처리를 코드로 확장하려는 곳에 맞는다 | MTA |
| Dovecot | 메일함을 보관하고 IMAP·POP3로 열어 주는 저장 계층 | MDA |
| Rspamd | 밀터로 붙어 점수 기반 스팸 판정·DKIM 서명을 하는 필터 | 필터 |
| Mailu | Postfix·Dovecot·Rspamd를 컨테이너로 쪼개 묶은 도커 우선 메일 배포판 | 배포판 |
| Mailcow | 같은 조합에 SOGo 그룹웨어·감시·방화벽까지 얹은 풀세트 배포판 | 배포판 |
| Docker Mailserver | 한 컨테이너 안에 Postfix+Dovecot+Rspamd를 담은 단일 이미지 배포판 | 배포판 |
| Stalwart | SMTP·IMAP·JMAP를 하나의 Rust 바이너리로 처리하는 신형 올인원 서버 | MTA+MDA |
| Postal | 앱이 쓰는 발송 서버 — 큐·추적·웹훅·API를 갖춘 자체호스팅 SendGrid | 발송 플랫폼 |
| Mailpit | 나가는 메일을 전부 가로채 웹UI로 보여 주는 개발용 가짜 SMTP | 개발용 |
2. 리눅스에서 실제로 어떻게 도는가
여기서부터가 이 글을 쓴 이유입니다. 위 표는 어디서든 볼 수 있지만, "그래서 리눅스에서 어떤 모양으로 뜨는가" 는 직접 띄워 보기 전에는 감이 안 잡혔습니다. 세 MTA를 각각 도커 컨테이너에 올리고 프로세스 트리를 그대로 떴습니다.

2-1. Postfix — 마스터 한 명과 필요할 때만 태어나는 자식들
Postfix 3.7.11을 띄우고 아무것도 하지 않은 상태입니다.
$ ps -eo pid,ppid,user,comm --sort=pid
PID PPID USER COMMAND
690 1 root master
691 690 postfix pickup
692 690 postfix qmgr
단 세 개입니다. master만 root이고 나머지는 비특권 postfix 사용자입니다. 그리고 각 프로세스는 스레드가 정확히 1개입니다(ps -eLo로 확인).
여기에 SMTP 세션을 하나 열고 다시 봤습니다.
690 1 root master
691 690 postfix pickup
692 690 postfix qmgr
700 690 postfix smtpd ← 접속이 들어오자 태어남
701 690 postfix proxymap
702 690 postfix tlsmgr
접속을 받는 smtpd는 미리 떠 있지 않고 master가 그때 fork 합니다. 이어서 메일을 한 통 큐에 넣자 또 늘어납니다.
709 690 postfix cleanup
711 690 postfix trivial-rewrite
712 690 postfix smtp
713 690 postfix bounce
715 690 postfix showq
이게 Postfix의 설계입니다. /etc/postfix/master.cf 에 서비스 목록이 선언되어 있고, master는 필요한 순간에 필요한 역할만 프로세스로 띄웁니다.
smtp inet n - y - - smtpd
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
qmgr unix n - n 300 1 qmgr
역할마다 프로세스를 나누는 이유는 성능이 아니라 격리입니다. 외부 입력을 직접 만지는 smtpd가 뚫려도 그 프로세스는 비특권이고, 큐 디렉터리 권한이 다시 한 번 막습니다.
drwx------ postfix root /var/spool/postfix/incoming
drwx------ postfix root /var/spool/postfix/active
drwx------ postfix root /var/spool/postfix/deferred
drwx-wx--T postfix postdrop /var/spool/postfix/maildrop ← 쓰기만 되고 읽기는 안 됨
drwx--s--- postfix postdrop /var/spool/postfix/public
maildrop의 -wx 와 sticky 비트(T)에 주목할 만합니다. 일반 사용자가 메일을 넣을 수는 있어도 남의 메일을 읽거나 지울 수는 없게 파일시스템 권한만으로 경계를 만들어 둔 것입니다.
2-2. OpenSMTPD — 처음부터 역할이 고정된 파이프라인
같은 방식으로 OpenSMTPD 6.8.0p2를 띄웠습니다.
$ ps -eo pid,ppid,user,args --sort=pid
PID PPID USER COMMAND
282 1 root smtpd -d ← 부모만 root
284 282 opensmtpd smtpd: klondike ← 암호 연산
285 282 opensmtpd smtpd: control ← 제어 소켓
286 282 opensmtpd smtpd: lookup ← 이름·별칭 조회
287 282 opensmtpd smtpd: pony express ← SMTP 송수신 엔진
288 282 opensmtpd smtpd: queue ← 큐 관리
289 282 opensmtpd smtpd: scheduler ← 재시도 스케줄
Postfix와 결이 비슷하지만 차이가 분명합니다. 자식 6개가 기동 직후 전부 떠 있고, 그 수가 트래픽에 따라 변하지 않습니다. 역할도 queue·scheduler·lookup처럼 더 굵게 잘려 있습니다. Postfix가 "필요할 때 태어나는 20여 종의 작은 데몬"이라면 OpenSMTPD는 "처음부터 자리를 잡은 6개의 고정 파이프라인"입니다. 각 프로세스는 역시 1스레드입니다.
설정 파일의 차이도 그대로 성격을 보여 줍니다. 위 서버를 띄운 /etc/smtpd.conf 전문이 이게 전부입니다.
listen on 127.0.0.1 port 2525
action "local" maildir alias <aliases>
action "relay" relay
match for local action "local"
match from local for any action "relay"
Postfix의 main.cf가 수백 개 파라미터를 가진 것과 비교하면, OpenSMTPD가 읽히는 설정을 목표로 삼았다는 게 여기서 드러납니다.
2-3. Haraka — 프로세스는 둘, 스레드는 스물둘
Haraka 3.3.2(Node.js v20.20.2)는 완전히 다른 그림이었습니다.
PID PPID USER COMMAND
199 1 root node /usr/local/bin/haraka -c /tmp/h ← 런처
211 199 root node .../Haraka/haraka.js -c /tmp/h ← 워커
pid 199 Threads: 11 VmRSS: 60,148 kB
pid 211 Threads: 11 VmRSS: 60,364 kB
프로세스는 2개인데 각각 스레드가 11개입니다. 앞의 두 MTA가 "프로세스 여럿 × 1스레드"였던 것과 정확히 반대입니다.
이 11개는 SMTP 세션을 처리하는 스레드가 아닙니다. 접속 처리는 단일 이벤트 루프가 전부 맡고, 나머지 스레드는 V8의 백그라운드 작업과 libuv 스레드풀(파일 I/O·DNS 같은 블로킹 호출용)입니다. 즉 동시 접속 1,000개를 받아도 스레드는 11개 그대로입니다.
Haraka는 기동할 때 이런 경고도 남깁니다.
[WARN] [server] smtp.ini.nodes unset, using 1, see .../Performance-Tuning
Node의 cluster로 워커를 CPU 수만큼 늘리라는 안내입니다. 즉 Haraka의 확장 축은 스레드가 아니라 프로세스(워커) 복제입니다.
2-4. 숫자로 나란히
| Postfix 3.7.11 | OpenSMTPD 6.8.0p2 | Haraka 3.3.2 | |
|---|---|---|---|
| 유휴 시 프로세스 | 3개 | 7개(부모+6) | 2개 |
| 프로세스당 스레드 | 1 | 1 | 11 |
| 유휴 RSS 합계 | 16.1 MB | 51.8 MB | 120.5 MB |
| 부하 시 늘어나는 것 | 프로세스 수(온디맨드 fork) | 늘지 않음(고정) | 이벤트 루프 안의 소켓 수 |
| 확장 방식 | master가 fork | 프로세스 복제 | 워커 프로세스 복제(nodes) |
| 특권 분리 | master만 root | 부모만 root | 별도 분리 없음 |
| 설정 성격 | 파라미터 수백 개 | 문장형 규칙 몇 줄 | JavaScript 플러그인 |
| 확장 지점 | milter·전송 맵 | 필터 훅 | 훅마다 JS 코드 |
메모리 차이가 눈에 띕니다. C로 짜인 두 MTA가 16~52MB인데 Node 기반은 유휴 상태로 120MB를 씁니다. 소규모 서버에서는 의미가 있지만, 요즘 노드 사양에서는 선택 기준이 되기엔 약한 차이이기도 합니다. 실제 선택은 대개 확장 지점이 무엇이냐로 갈립니다.
참고로 이 "프로세스를 나눌 것인가, 스레드를 나눌 것인가"는 메일 서버만의 주제가 아닙니다. 같은 갈림길을 커널 수준에서 확인한 글을 따로 썼습니다 → 프로세스·스레드·뮤텍스를 리눅스에서 직접 확인하기
3. 메일 한 통이 리눅스를 지나가는 경로
프로세스 목록만으로는 흐름이 안 보여서, 없는 도메인으로 메일을 한 통 보내고 큐를 들여다봤습니다.
$ sendmail -f sender@example.com nobody@invalid-domain-xyz.test
$ postqueue -p
-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
C0DFF34F1C 268 Sat Aug 1 19:02:48 sender@example.com
(Host or domain name not found. Name service error for
name=invalid-domain-xyz.test type=MX: Host not found, try again)
nobody@invalid-domain-xyz.test
$ find /var/spool/postfix -type f
/var/spool/postfix/deferred/C/C0DFF34F1C
MX 조회에 실패하자 메일은 버려지지 않고 deferred 디렉터리에 파일 하나로 남았습니다. 정리하면 경로는 이렇습니다.
로컬 발송 외부 수신
sendmail :25 → smtpd
│ │
▼ │
maildrop ──pickup──▶ cleanup ◀─────┘ 헤더 정리·DKIM·milter
│
▼
incoming
│
qmgr 큐 관리자가 배달을 지시
│
┌───────────┼───────────┐
▼ ▼ ▼
active deferred bounce
│ │
▼ (재시도)
smtp/local ──▶ 외부 MX 또는 로컬 메일함
이 그림에서 실무적으로 중요한 지점은 deferred가 디스크 위의 상태라는 사실입니다. 메일 서버는 stateless가 아닙니다. 이 성질 하나가 뒤에서 쿠버네티스 설계를 통째로 흔듭니다.
4. 배포판은 결국 무엇을 묶은 것인가
Mailu와 Mailcow는 "메일 서버"라기보다 위 부품들의 조립 명세입니다. 말로 설명하는 대신 두 프로젝트의 compose 파일을 받아 서비스 목록을 그대로 뽑았습니다.
Mailu (core 구성, 8개 서비스)
| 서비스 | 실체 | 역할 |
|---|---|---|
front |
nginx | 모든 프로토콜의 앞단 프록시 |
smtp |
Postfix | MTA |
imap |
Dovecot | 메일함·IMAP |
antispam |
Rspamd | 스팸·DKIM |
oletools |
oletools | 첨부 문서 악성코드 검사 |
admin |
자체 웹앱 | 계정·도메인 관리 UI |
resolver |
Unbound | 전용 DNS 리졸버 |
redis |
Redis | 상태·레이트리밋 |
Mailcow (18개 서비스) — Mailu의 구성에 더해 sogo(그룹웨어), clamd(바이러스), mysql(MariaDB), memcached, php-fpm, acme(인증서 자동화), netfilter(방화벽·차단), watchdog(감시), dockerapi, olefy, ofelia(크론), postfix-tlspol 이 붙습니다.
여기서 두 가지가 분명해집니다.
- 어느 배포판이든 심장은 Postfix + Dovecot + Rspamd 입니다. 배포판이 파는 건 MTA가 아니라 조립과 운영 편의입니다.
- Mailcow는 그룹웨어 제품에 가깝습니다. 18개 서비스 중 절반 이상이 SMTP 자체와 직접 관련이 없습니다.
netfilter가 호스트 iptables를 직접 만지고dockerapi가 도커 소켓을 잡는 구조라, 이 성격이 뒤의 쿠버네티스 판단에서 결정적으로 작용합니다.
5. 그래서 쿠버네티스에서는 어떻게 조합하나
5-1. 옮기기 전에 걸리는 네 가지
메일 서버를 클러스터에 올릴 때 실제로 발목을 잡는 건 매니페스트 문법이 아니라 메일 프로토콜이 인프라에 요구하는 것들입니다.
| 제약 | 왜 문제인가 | 쿠버네티스에서의 대응 |
|---|---|---|
| 큐가 디스크 상태 | deferred의 메일은 파드가 죽으면 같이 사라진다 |
Deployment가 아니라 StatefulSet + PVC. ReadWriteOnce면 복제본을 함부로 못 늘린다 |
| 포트 25 아웃바운드 | 대부분의 클라우드가 25번 나가는 트래픽을 막는다 | 확인 먼저. 막혔다면 릴레이(SES·Mailgun) 경유가 현실적 |
| 발신 IP와 PTR | 수신 서버는 IP 평판과 역방향 DNS를 본다. 노드마다 IP가 다르면 평판이 흩어진다 | 고정 egress IP 확보. externalTrafficPolicy: Local, 전용 노드 배치 |
| 인증서·도메인 | STARTTLS 인증서가 웹과 다른 경로로 필요 | cert-manager + 인증서 시크릿 마운트 |
이 넷 중 포트 25 아웃바운드가 가장 먼저 확인할 항목입니다. 여기서 막히면 그 뒤 설계가 전부 무의미해집니다.
5-2. 목적별 조합
| 목적 | 조합 | 이유 |
|---|---|---|
| 앱이 알림 메일만 보낸다 | 클러스터 안 Postfix 릴레이(널 클라이언트) → 외부 SMTP | 큐만 잠깐 들고 실제 발송은 위임. 가장 가볍고 실패 지점이 적다 |
| 발송을 직접 하고 추적도 필요 | Postal (web / smtp / worker + MariaDB) | 큐·재시도·바운스·웹훅·API가 이미 제품 안에 있다 |
| 사람이 쓰는 메일함까지 운영 | Stalwart 또는 Mailu | Stalwart는 단일 바이너리로 SMTP·IMAP·JMAP을 함께 처리. Mailu는 이미 서비스별 컨테이너라 파드로 가르기 쉽다 |
| 개발·테스트 환경 | Mailpit 단일 파드 | 실제 발송 없이 전량 캡처. 스테이징 사고 방지 |
| 클러스터 내부 SMTP 정책 통제 | kube-mail 류 | 앱은 평범한 SMTP로 보내고 정책·릴레이는 중앙에서 |
| Mailcow 이식 | 권장하지 않음 | 호스트 iptables·도커 소켓 의존이 파드 모델과 어긋난다. 별도 VM이 낫다 |
5-3. 배포판을 그대로 옮기면 안 되는 이유
Mailu·Mailcow는 한 대의 호스트에서 도커 컴포즈로 돌아가는 것을 전제로 설계되어 있습니다. 그 전제가 쿠버네티스와 부딪히는 지점이 분명합니다.
- 여러 컨테이너가 같은 볼륨을 공유합니다. 노드가 여럿이면
ReadWriteMany스토리지가 필요해집니다. - 컨테이너끼리 고정된 이름으로 서로를 찾습니다. Service 이름과 맞추는 재작업이 생깁니다.
- Mailcow의
netfilter는 호스트 방화벽을 직접 조작합니다. 파드에서 할 일이 아닙니다.
그래서 제 판단은 이렇습니다. 묶음을 통째로 이식하기보다, 필요한 역할만 골라 파드로 세우는 편이 낫습니다. 메일함이 필요 없다면 Dovecot도 Rspamd도 SOGo도 필요 없습니다.
6. 정리, 그리고 다음
- 이름들은 경쟁자가 아니라 층입니다. MTA(Postfix·OpenSMTPD·Haraka) / 저장(Dovecot) / 필터(Rspamd) / 묶음(Mailu·Mailcow) / 발송 플랫폼(Postal) / 개발용(Mailpit).
- 리눅스에서의 모양은 셋으로 갈립니다. Postfix = 온디맨드 다중 프로세스(유휴 3개, 16MB), OpenSMTPD = 고정 6역할 파이프라인(52MB), Haraka = 2프로세스 × 11스레드 이벤트 루프(120MB).
- 앞의 둘은 프로세스 분리로 권한을 격리하고, Haraka는 이벤트 루프로 동시성을 흡수합니다. 같은 문제를 푸는 다른 답입니다.
- 배포판의 심장은 어디나 Postfix + Dovecot + Rspamd 이고, 파는 것은 조립입니다.
- 쿠버네티스로 옮길 때 진짜 제약은 매니페스트가 아니라 큐의 디스크 상태 · 포트 25 · 발신 IP 평판 · 인증서 넷입니다.
저는 처음엔 "앱의 발송을 직접 처리하고 추적까지" 를 목표로 잡았는데, 조사하는 동안 "기왕이면 메일함까지 제대로" 로 목표가 넓어졌습니다. 그 한 줄이 후보를 통째로 갈랐습니다 → 쿠버네티스 SMTP #1 — Postal이냐 Stalwart냐
관련해서 함께 읽으면 좋은 글:
- [로컬 서버 운영기] Docker Compose에서 K3s로 — 컴포즈 전제 스택을 쿠버네티스로 옮길 때 겪은 문제들
- [로컬 서버 운영기] 집 PC를 공개 서버로 — 포트를 열지 않고 외부에 노출하는 방법(메일의 25번 문제와 직결됩니다)
- [OKE 인프라 학습 시리즈] arm64 노드에 실제 서비스 얹기 — 아키텍처가 안 맞는 이미지를 만났을 때
- 홈서버 하나로 관측성부터 GitOps까지 — 이 클러스터의 관측 스택
참고 (1차 출처)
- Postfix 공식 문서 — Architecture Overview,
master.cf매뉴얼 - OpenSMTPD —
smtpd.conf(5) - Haraka — Performance Tuning 위키
- Mailu docker-compose (core flavor) · Mailcow docker-compose.yml
- Rspamd · Dovecot · Mailpit
- 측정 환경: Docker 29.5.3 /
debian:bookworm-slim/ 커널 6.12.76-linuxkit aarch64 / Postfix 3.7.11 · OpenSMTPD 6.8.0p2 · Haraka 3.3.2(Node v20.20.2)
답글 남기기