,

[메일 인프라] Postfix·OpenSMTPD·Haraka·Mailu·Mailcow는 각각 무엇을 맡나 — 리눅스 프로세스 트리로 본 메일 서버 지형도

이 글의 위치: 기술 블로그 → 인프라 · 「쿠버네티스 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를 각각 도커 컨테이너에 올리고 프로세스 트리를 그대로 떴습니다.

Postfix·OpenSMTPD·Haraka의 리눅스 프로세스 트리 비교 — 유휴 프로세스 수, 프로세스당 스레드 수, RSS 실측

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 이 붙습니다.

여기서 두 가지가 분명해집니다.

  1. 어느 배포판이든 심장은 Postfix + Dovecot + Rspamd 입니다. 배포판이 파는 건 MTA가 아니라 조립과 운영 편의입니다.
  2. 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냐

관련해서 함께 읽으면 좋은 글:


참고 (1차 출처)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다