로컬서버에 메일 세우기
받는 일은 조직 기계가, 보내는 일은 무료 중계가 맡습니다
조직 로컬서버에 메일 수발신 시스템을 세우는 방법을 정리한 글입니다. 품앗이 사무실 서버(2026-10-15, 수신 직결형)와 한 조합의 NAS(2026-10-17, 수신 우회형) 두 곳에 실제로 세우며 확인한 것을 바탕으로 했습니다. 끝에는 구글 워크스페이스 비영리판을 받지 못하는 조직과 하이웍스를 쓰는 조직이 함께 읽을 만한 내용을 붙였습니다. 기계를 세우는 과정은 로컬서버, 조직 명의의 기계에 있습니다.
1. 왜 직접 세우는가
메일은 조직 기록의 가장 큰 덩어리입니다
조직에서 바깥과 주고받은 일의 기록은 대부분 메일함에 남습니다. 계약, 공문, 견적, 회원 문의, 서비스 가입 인증까지 다 거기 있습니다. 그런데 그 메일함은 대개 남의 서비스 안에 있습니다. 조직 AI가 「지난달 견적 메일 찾아 정리해」를 하려면 메일함을 읽을 수 있어야 하는데, 남의 서비스는 그 길을 열어 주지 않거나 비싸게 엽니다.
밖에 맡긴 시스템 위에는 AI를 얹기 어렵습니다. 자체 위키를 세운 이유와 같습니다. 메일이 조직 기계 안에 있으면 표준 규격(IMAP·JMAP)으로 조직 AI가 직접 읽을 수 있고, 무엇을 읽게 할지도 조직이 정합니다.
돈 문제도 있습니다
한 조합은 국내 그룹웨어 메일에 매달 27만 원(부가세 별도)을 내고 있었습니다. 주소는 116개였지만 날마다 메일을 쓰는 사람은 다섯 명 남짓이었습니다. 계정 수로 값을 매기는 서비스에서는 주소가 늘수록 비용이 따라 늘어납니다. 자체 서버에서는 주소 116개를 다 만들어도 추가 비용이 없습니다.
예전엔 왜 안 했나
「메일서버는 직접 돌리면 안 된다」는 말이 오래 상식이었습니다. 틀린 말이 아닙니다. 보내는 메일이 상대 서버에서 스팸으로 막히기 때문입니다. 받는 쪽은 그렇게 어렵지 않습니다. 그래서 이 글의 설계는 둘을 나눕니다.
- 받기·보관·읽기는 조직 기계가 합니다. 기록이 조직 안에 남습니다.
- 보내기는 무료 발송 중계(Resend)에 맡깁니다. 상대 서버가 믿어 주는 길로 나갑니다.
2. 왜 직접 보내면 안 되는가 — 실측
품앗이는 사무실 서버에서 지메일로 직접 보내 봤습니다(2026-10-14).
| 시험 | 조건 | 지메일 응답 |
|---|---|---|
| 1 | 아무 인증 없이 | 550 5.7.26 인증 안 된 발신자 |
| 2 | 발신 인증 3종(SPF·DKIM·DMARC)을 다 붙인 뒤 | 550 5.7.25 이 IP에 PTR 레코드가 없다 |
인증을 다 갖춰도 PTR 하나로 문 앞에서 거부됩니다. PTR은 「이 IP는 이 이름의 메일서버다」라는 역방향 기록인데, 통신사가 걸어 줘야 합니다. 일반 사무실 회선은 IP가 바뀌는 동적 회선이라 PTR을 걸 수 없습니다. 우리가 만나는 조직 회선도 거의 다 그렇습니다.
그래서 직접 발송은 표준이 아닙니다. 고정 IP 업무용 회선에 통신사 PTR까지 있는 조직만 예외입니다. 「DKIM 붙이면 되겠지」로 다시 열지 마십시오. PTR 검사는 인증보다 앞에서 일어납니다.
발송 중계 고르기
| 길 | 무료 한도 | 카드 | 비고 |
|---|---|---|---|
| Resend | 월 3,000통·하루 100통·도메인 3개 | 필요 없음 | 품앗이 표준. SMTP로 바로 붙는다 |
| Brevo | 하루 300통 | 필요 없음 | 계정 심사가 있다 |
| SMTP2GO | 월 1,000통 | 필요 없음 | |
| Cloudflare Email Sending | Workers 유료($5/월)에 월 3,000통 | 필요 | 베타. 발신 도메인에 Email Routing을 켜야 한다 |
| Amazon SES | 없음(1,000통당 $0.10) | 필요 | 새 계정은 제한 모드부터 |
조직 메일 수요는 생각보다 작습니다. 품앗이 사무실은 90일 동안 서비스 인증 메일이 9통이었습니다. 사람이 쓰는 업무 메일을 더해도 소규모 조직은 하루 100통에 잘 닿지 않습니다. 회원 소식지를 수천 통씩 보내는 조직이라면 소식지만 별도 서비스로 빼는 편이 맞습니다.
🔴 발송 중계 계정은 그 조직 이름으로 만듭니다. 품앗이 계정에 남의 조직 도메인을 얹으면 그 조직은 자기 메일을 끊을 열쇠를 갖지 못합니다.
3. 먼저 재 볼 것 — 회선이 25번을 받아 주는가
바깥 메일서버는 25번 포트로 메일을 넘겨줍니다. 이게 회선에서 막혀 있느냐에 따라 설계가 둘로 갈립니다.
반드시 바깥 회선에서 잽니다. 같은 사무실 안에서 자기 공인 IP로 붙으면 공유기가 안에서 돌려보내기 때문에 판정이 안 됩니다. 다른 사무실 서버나 휴대폰 핫스팟에 붙인 노트북에서 합니다.
# 서버에서: 25번에서 잠깐 기다린다 (메일서버 설치 전)
sudo python3 -c "import socket;s=socket.socket();s.bind(('0.0.0.0',25));s.listen();print(s.accept())"
# 바깥 회선에서
nc -vz <공인IP> 25| 결과 | 갈래 |
|---|---|
| 바깥에서 붙는다 | 갈래 A — 수신 직결. 공유기에서 25번을 서버로 연다 |
| 안 붙는다(회선 차단·CGNAT·공유기 손댈 수 없음) | 갈래 B — 수신 우회. Cloudflare가 대신 받아 터널로 들여온다. 포트를 하나도 안 연다 |
품앗이 사무실(KT 회선)은 25번이 열려 있었고, 한 조합 사무실 회선은 25·465·993이 전부 막혀 있었습니다. 회선마다 다르니 매번 잽니다.
4. 갈래 A — 수신 직결
[받기] 발신자 → MX(mail.<조직도메인>) → 공유기 25 → 서버 Stalwart
[보내기] 직원 메일앱·웹메일 → Stalwart 465 → Resend → 상대 서버
[읽기] 메일앱 IMAP 993 · 웹메일
4-1. 공유기 문 열기
공유기 관리자 비밀번호가 없어도 UPnP가 켜져 있으면 서버가 스스로 포트를 엽니다. 방법과 스크립트는 로컬서버에 화상회의 세우기 3-1절과 같습니다. 프로토콜만 TCP로, 포트를 25로 바꿉니다. 사람들이 바깥에서 메일앱으로 쓰려면 465(보내기)와 993(읽기)도 같이 엽니다.
품앗이는 5분마다 도는 타이머 하나가 두 일을 합니다. UPnP 매핑을 다시 걸고, 공인 IP가 바뀌었으면 Cloudflare DNS의 mail. A 레코드를 고칩니다. 동적 회선이라 이 타이머가 없으면 어느 날 조용히 메일이 끊깁니다.
4-2. 인증서
메일서버는 웹이 아니라서 Cloudflare 터널이 인증서를 대신 맡아 주지 않습니다. Let’s Encrypt를 DNS 인증(DNS-01)으로 받습니다. 80번 포트를 열 필요가 없습니다.
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/cf.ini -d mail.<조직도메인>갱신 훅에서 인증서를 메일서버 폴더로 복사하고 메일서버를 다시 띄우게 해 둡니다.
5. 갈래 B — 수신 우회 (포트 개방 0)
회선이 25번을 막았거나 공유기를 만질 수 없는 조직의 길입니다. Cloudflare Email Routing이 메일을 대신 받고, Email Worker가 그 원문을 HTTPS로 터널 안까지 들고 들어옵니다. 서버 안의 작은 다리 프로그램이 그것을 다시 SMTP로 바꿔 메일서버에 넣습니다.
[받기] 발신자 → MX = Cloudflare Email Routing → Email Worker
→ https://mailin.<조직도메인>/inbound (공유 비밀 헤더) → Cloudflare 터널
→ 다리(mailin) → Stalwart 25
[보내기] Stalwart → Resend
[읽기] 웹메일(터널로 공개) · JMAP
5-1. Email Worker
받은 메일을 통째로 넘기고, 없는 주소면 발신자에게 정직하게 반송하고, 서버가 잠깐 죽었으면 예외를 던집니다. 예외를 던지면 Cloudflare가 발신자에게 「잠시 후 다시」를 돌려줘 메일이 사라지지 않습니다.
export default {
async email(message, env) {
const raw = await new Response(message.raw).arrayBuffer();
const r = await fetch(env.MAILIN_URL, {
method: "POST",
headers: {
"X-Mailin-Key": env.MAILIN_KEY,
"X-Envelope-From": message.from,
"X-Envelope-To": message.to,
"Content-Type": "message/rfc822",
},
body: raw,
});
if (r.status === 550) {
message.setReject((await r.text()).slice(0, 200) || "recipient rejected");
return;
}
if (!r.ok) throw new Error("mailin " + r.status);
},
};5-2. 다리 (mailin)
파이썬 표준 라이브러리만 씁니다. 받은 원문을 메일서버 25번에 SMTP로 넣고, 메일서버가 5xx(없는 주소)로 거절하면 550을, 그 밖의 실패는 500을 돌려줍니다. Worker는 550이면 반송, 500이면 재시도로 처리합니다. 공유 비밀 헤더가 틀리면 401로 끊습니다. 70줄 남짓이라 여기 옮기지 않습니다. 필요하면 품앗이에 요청하십시오.
5-3. 병행 운영을 위한 서브도메인
기존 메일 서비스를 당장 끊을 수 없다면 Email Routing을 조직 도메인 꼭대기가 아니라 서브도메인(mx.<조직도메인>)에 켭니다. 그러면 꼭대기 MX는 기존 서비스 그대로이고, 기존 서비스에서 각 계정을 <아이디>@mx.<조직도메인>으로 전달하면 우리 서버에도 사본이 쌓입니다. 다리가 봉투 수신자의 @mx.<조직도메인>을 @<조직도메인>으로 바꿔 넣습니다.
서브도메인에만 켜면 Cloudflare 화면에 misconfigured가 뜹니다. 꼭대기에 레코드가 없다는 뜻일 뿐이니 정상입니다.
6. 메일서버 설치 (두 갈래 공통)
메일서버는 Stalwart를 씁니다. 받기·보내기·보관·스팸 거르기·웹 관리 화면이 컨테이너 하나에 들어 있습니다. 예전처럼 Postfix·Dovecot·스팸필터를 따로 엮을 필요가 없습니다. 품앗이는 두 현장 모두 v0.16.26을 썼습니다.
services:
stalwart:
image: stalwartlabs/stalwart:v0.16.26
hostname: mail.<조직도메인>
restart: unless-stopped
env_file: ./stalwart.env # STALWART_RECOVERY_ADMIN=admin:<비밀번호>
volumes:
- ./etc:/etc/stalwart
- ./data:/var/lib/stalwart
ports: # 갈래 A 만. 갈래 B 는 ports 를 두지 않는다
- "25:25"
- "465:465"
- "993:993"
# 관리 화면(웹) 포트는 127.0.0.1 에만 묶고 터널로 낸다관리 화면이 컨테이너 안에서 쓰는 포트는 판마다 다를 수 있으니 공식 문서에서 확인하고 묶습니다. 갈래 B에서는 같은 도커 네트워크 안의 다리·웹메일·터널만 메일서버에 닿고, 호스트 포트는 하나도 열지 않습니다.
6-1. 설치할 때 할 일
- 복구 관리자를 env로 고정합니다. 첫 기동의 임시 비밀번호는 재시작마다 바뀝니다. 인증서 훅이 재시작하는 순간 저장해 둔 비밀번호가 안 먹습니다.
- 설치 마무리(Bootstrap) 단계에서 상설 관리자 비밀번호가 출력에 한 번만 찍힙니다. 그 자리에서 받아 적습니다.
- 도메인 등록 → DKIM 자동 생성 → DNS 게시.
- 발송 중계 지정. 관리 화면에서 Resend를 중계 경로(
smtp.resend.com:465, 암묵 TLS, 사용자resend, 비밀번호=API 키)로 만들고, 바깥으로 나가는 기본 경로를 그 중계로 바꿉니다. 키는 파일로 두고 파일 끝 개행을 지웁니다. - 🔴 설정을 바꾼 뒤 재시작합니다. 인증서와 발송 경로는 재시작 전엔 안 먹습니다. 품앗이 첫 메일은 재시작 전이라 중계가 아니라 직접 발송으로 나갔습니다.
- 계정과 주소. 사람마다 계정을 만들고, 부서·역할 주소는 별칭으로 붙입니다. 주소 하나 만드는 데 비용이 없습니다. catch-all(모르는 주소도 한 함으로 받기)은 이행기에만 씁니다.
- 로그인 실패 차단을 조입니다. 기본값보다 낮게(품앗이는 하루 10회) 두고, 프록시 뒤라면 실제 접속 IP로 차단되게 신뢰 대역을 지정합니다.
6-2. DNS 레코드
| 레코드 | 값 | 누가 정하나 |
|---|---|---|
mail A | 서버 공인 IP (갈래 A) | 타이머가 갱신 |
| 꼭대기 MX | 갈래 A: mail.<조직도메인> / 갈래 B: Cloudflare 3건 | 컷오버 날 |
| SPF | 갈래 A v=spf1 mx ~all / 갈래 B Cloudflare include | |
| DKIM | Stalwart가 만든 것 + Resend가 준 것 | |
_dmarc | p=none으로 시작 → 보고를 며칠 본 뒤 quarantine | |
| Resend 쪽 | send. MX·SPF, 확인용 CNAME | Resend 화면이 알려 준다 |
🔴 바꾸기 전에 DNS 전체를 스냅샷으로 떠 둡니다. 되돌리기 값이 그 파일입니다. 기존 메일 서비스가 아직 살아 있으면 꼭대기 MX·SPF는 그 서비스 몫이니 손대지 않고, 서브도메인에만 더합니다.
6-3. 사람이 읽는 길
- 웹메일: Roundcube 컨테이너를 같이 띄우고 터널로 냅니다. 한국어 화면이 나옵니다. 메일앱 설정이 어려운 사람에게 가장 쉽습니다.
- 메일앱(폰·PC): IMAP 993·SMTP 465. 갈래 A는 공유기로 열고, 갈래 B는 터널이 웹만 나르므로 테일넷 안에서만 쓰거나 웹메일로 갑니다.
- 조직 AI: JMAP 또는 IMAP으로 읽습니다. 사람 계정을 빌려 주지 말고 읽기용 계정을 따로 만듭니다.
7. 다 됐는지 확인하기
「컨테이너가 떴다」와 「메일이 오간다」는 다릅니다. 판정 기준은 시험 전에 정해 둡니다.
| 확인 | 통과 기준 |
|---|---|
| 받기 | 바깥 메일(지메일 등) → 우리 주소 받은편지함 도착, 스팸함이면 실패 |
| 보내기 | 우리 주소 → 지메일 받은편지함, 원문 헤더에 spf=pass dkim=pass dmarc=pass, From이 조직 도메인 |
| 반송 | 없는 주소로 보내면 발신자에게 반송이 간다 |
| 인증 메일 | 조직이 쓰는 서비스(로그인·가입 확인)의 메일이 실제로 와서 로그인까지 된다 |
| 백업 | 메일 데이터 폴더가 백업 축에 들어갔고, 백업본이 실제로 생겼다 |
품앗이는 수신 11초, 발신 인증 3종 통과, 클로드 로그인 매직링크 수신까지 확인했습니다. 네이버·다음 수신함은 우리가 가진 게 없어 재지 못했습니다. 재지 못한 것을 통과로 치지 않습니다.
8. 함정
- 로그가 컨테이너 안 파일로 갑니다.
docker logs엔 부팅 직후 몇 줄만 보입니다. 로그 자리를 볼륨으로 잡아 둡니다. - 인증 없는 시험 메일은 스팸함으로 갑니다. 서버 안에서 손으로 쏜 시험 메일이 받은편지함에 없다고 고장으로 읽지 마십시오. 받은편지함과 스팸함 둘 다 봅니다.
- 테일넷 안에서는
mail.<도메인>이 공인 IP로 풀려 465·993에 안 닿을 수 있습니다. 같은 기계 안의 컨테이너는extra_hosts로 내부 IP를 가리키게 합니다. - 스팸 블랙리스트 조회가 공용 DNS에서 막힙니다(
DBL_BLOCKED_OPENRESOLVER). 서버에 자체 재귀 DNS(unbound)를 두는 것이 해법입니다. - 계정 생성 API의 모양이 문서와 다릅니다(목록이 아니라 번호 붙은 맵). 기존 계정을 스냅샷으로 떠서 그 모양을 그대로 씁니다.
- 새 데이터 폴더는 백업 대상에 직접 추가해야 합니다. 백업 도구는 적힌 폴더만 가져갑니다.
- 백업 MX가 없습니다. 서버가 죽어 있는 동안 보내는 쪽 서버가 메일을 쥐고 며칠간 재시도합니다. 보통은 살아난 뒤 들어오지만, 며칠 넘게 죽어 있으면 반송됩니다. 정전 자동 켜짐과 서버 건강 감시가 메일서버의 선행 조건입니다.
9. 옮기기와 병행
이미 쓰던 메일이 있는 조직이라면 순서가 중요합니다.
- 병행 수신. 기존 서비스는 그대로 1차로 두고 사본만 우리 서버로 받습니다(갈래 B 서브도메인, 또는 기존 서비스의 전달 기능).
- 과거 메일 옮기기.
imapsync로 폴더째 옮깁니다. 품앗이는 227통을 오류 없이 옮겼고, 폴더별 통수를 원본과 대조했습니다. 끊기 직전에 한 번 더 돌리면 늘어난 것만 옮깁니다. - 신뢰 쌓기. 매달 「우리 서버가 받은 건수·용량」을 조직에 보고합니다.
- 컷오버. 꼭대기 MX를 우리 쪽으로 돌립니다. 롤백 값(기존 MX·SPF)을 미리 적어 둡니다.
- 해지. 해지는 조직이 합니다.
🔴 컷오버 날짜는 그 조직이 정합니다. 다 만들어 놓았다고 품앗이가 넘어오라고 재촉하지 않습니다. 그 조합은 지금 1단계(병행)에 있습니다.
10. 시사점 — 구글 워크스페이스 비영리판을 못 받는 조직
구글 워크스페이스 비영리판은 무료이고 좋은 길입니다. 품앗이도 2026-10에 승인을 받았습니다. 하지만 이 길이 닫힌 조직이 많습니다.
- 하는 일로 자릅니다. 구글은 정부기관·학교·병원과 의료기관을 받지 않습니다. 신청 첫 화면에서 「의료기관이 아님」을 선언해야 하므로 의료기관을 운영하는 조직은 넘어가지 못합니다. 같은 검증사를 쓰는 앤스로픽 비영리 프로그램은 법인격으로 판단해 의료 쪽에 오히려 열려 있습니다. 「비영리법인이니 둘 다 되겠지」로 읽으면 안 됩니다.
- 법인격도 봅니다. 한국 법인은 사실상 국세청 사업자등록 또는 법원 법인등록으로 증빙합니다. 비영리 자격으로 등록되지 않은 조직은 들어가기 어렵습니다.
- 홈페이지로 심사합니다. 품앗이는 「조직과 도메인의 관계가 안 보인다」로 한 번, 「한 페이지짜리라 내용이 부족하다」로 또 한 번 거절됐고, 사명 선언문과 여러 쪽짜리 소개를 갖춰 세 번째에 통과했습니다. 신청에서 승인까지 3주 가까이 걸렸습니다.
이 길이 닫힌 조직에게 자체 메일서버는 차선책이 아니라 본길입니다. 메일은 조직 기계 위에, 보내기는 무료 중계로, 드라이브는 조직 기계의 파일서버(Nextcloud)로 갑니다. 들어가는 돈은 기계 한 대와 전기, 그리고 관리하는 사람의 시간입니다.
비영리판을 받은 조직이라도 메일만 자체로 둘 수 있습니다. 품앗이가 그렇게 합니다. 이때 한 가지를 조심합니다. 🔴 구글 쪽 Gmail 서비스를 꺼 두어야 합니다. 켜 둔 채 사용자를 만들면 구글 안에서 보낸 @조직도메인 메일이 우리 서버가 아니라 구글 함으로 새어 메일함이 둘로 갈라집니다. 구글은 드라이브·문서·화상회의·로그인 계정으로 쓰고, 메일은 조직 기계가 맡는 식으로 칸을 나눕니다.
11. 시사점 — 하이웍스 같은 국내 그룹웨어를 쓰는 조직
국내 그룹웨어는 메일·메신저·결재·근태를 한 묶음으로 팝니다. 그래서 메일만 떼어 내기 어렵다고 느낍니다. 실제로 갈라 보면 이렇습니다.
| 그룹웨어 기능 | 조직 기계 위의 대체 |
|---|---|
| 메일 | Stalwart + Resend + 웹메일(Roundcube) |
| 사내 메신저 | Mattermost (팀 에디션, 무료) |
| 결재·근태 | 조직 업무에 맞춘 작은 업무 앱 |
| 주소록·일정 | Stalwart의 CardDAV·CalDAV |
여기서 읽을 것 네 가지입니다.
- 주소 수와 사람 수를 따로 셉니다. 그룹웨어 요금은 계정 수로 붙는데, 주소의 대부분은 부서·역할·퇴직자 주소입니다. 자체 서버에서는 주소를 아무리 만들어도 값이 같습니다. 먼저 관리자 화면에서 주소 명단을 받아 「실제로 매일 쓰는 사람」을 세어 보십시오.
- 불편은 대개 그룹웨어의 고정된 규칙에서 옵니다. 한 조합에서 나온 요구는 「시간외근무 신청이 근무시간이 끝난 뒤에만 된다」는 것을 풀어 달라는 것이었습니다. 출근 전·점심·하루 여러 건은 신청할 수 없었습니다. 남의 제품 규칙은 우리가 못 고치지만, 조직 기계 위의 업무 앱은 그 조직의 일하는 방식에 맞출 수 있습니다.
- AI를 얹을 수 있느냐가 갈림길입니다. 메일·메신저·결재 기록이 조직 기계 안에 있으면 조직 AI가 「이번 주 미처리 결재」「지난달 거래처 메일」을 직접 찾습니다. 그룹웨어 안에 있으면 그 길이 막혀 있거나 따로 사야 합니다.
- 한 번에 갈아타지 않습니다. 메일이 끊기면 조직 일이 멈춥니다. 9절 순서대로 병행부터 시작하고, 우리 서버가 몇 달 동안 무엇을 받았는지 숫자로 보여 준 뒤에 조직이 넘어올 날을 정합니다.
12. 직접 세우면 지는 짐
자체 메일서버는 공짜가 아닙니다. 맡기기 전에 이것을 같이 봅니다.
- 기계가 죽으면 메일도 멈춥니다. 정전 자동 켜짐, 건강 감시, 백업이 먼저입니다.
- 관리할 사람이 있어야 합니다. 계정 추가, 비밀번호 초기화, 스팸 신고 대응을 누가 할지 정합니다. 처음에는 품앗이가 맡고, 조직 챔피언이 하나씩 넘겨받는 것이 현장 AX의 순서입니다.
- 하루 100통을 넘는 발송은 무료 중계 한도를 넘습니다. 그런 조직은 유료 등급이나 소식지 전용 서비스를 따로 둡니다.
- 도달률은 우리가 다 재지 못했습니다. 지메일은 확인했지만 네이버·다음은 아직입니다.
관련
- 로컬서버, 조직 명의의 기계 메일서버를 얹을 기계를 세우는 방법
- 로컬서버에 화상회의 세우기 같은 기계에 회의방을 얹는 방법, UPnP 스크립트
- 비영리조직 클로드 팀플랜 할인 신청하기 비영리 인증을 받는 다른 길
- 현장 AX, 품앗이 방식 현장AX 노드 모음