Back to blog

Local SMTP Relay với SendGrid trong Docker

dockersmtpsendgridhostingerresenddevopsemailself-hosting
Local SMTP Relay với SendGrid trong Docker

Bạn có một self-hosted app chạy trong Docker. Nó cần gửi email — password reset, notification, alert. Bạn có thể integrate trực tiếp SendGrid API vào mỗi app, nhưng vậy là phải thêm SDK dependency, API key, và email logic cho từng service.

Nếu app chỉ cần gửi email tới localhost:25 như một SMTP client truyền thống, rồi có thằng relay lo phần còn lại thì sao?

Đó chính xác là thứ chúng ta sẽ build: một local SMTP relay server nhận mail trên port 25 và forward qua SendGrid SMTP API. Các Docker container trên cùng host connect tới nó qua external fixed network — không expose ra internet, không config phức tạp.

Phiên bản mới của project cũng đã mở rộng ý tưởng này thêm một bước: ngoài SendGrid, relay còn có thể forward qua Hostinger hoặc Resend chỉ bằng cách đổi biến môi trường trong .env. Nói cách khác, bài viết này vẫn giữ walkthrough SendGrid làm ví dụ chính, nhưng kiến trúc đằng sau giờ đã linh hoạt hơn trước.

Bạn Sẽ Học Được Gì

✅ Tại sao local SMTP relay hữu ích cho self-hosted setup
✅ Cách configure Postfix làm SendGrid SMTP relay
✅ Cách dùng Docker external network cho container-to-container communication
✅ Cách connect bất kỳ Dockerized app nào tới relay trên port 25
✅ Cách mở rộng relay để đổi provider qua .env mà không sửa từng app
✅ Security best practice cho local SMTP relay


Tại Sao Cần Local SMTP Relay?

Vấn Đề

Mỗi app gửi email đều cần:

  • Email provider SDK hoặc SMTP configuration
  • API key hoặc credential lưu riêng cho từng service
  • Retry logic, error handling, rate limiting

Khi bạn có nhiều service — blog, monitoring tool, CI runner, home automation — việc manage email credential trong từng cái trở nên cực kỳ mệt mỏi.

Giải Pháp

Một SMTP relay duy nhất trên host mà:

  1. Listen trên port 25 (standard SMTP)
  2. Chỉ accept mail từ trusted Docker container
  3. Forward tất cả qua SendGrid SMTP server
  4. Handle retry, queuing, và delivery

App chỉ cần trỏ SMTP setting tới relay container — không SDK, không API key trong từng app.

Điểm đáng giá nhất của mô hình này là các app chỉ nói chuyện với smtp-relay:25. Vì vậy nếu sau này bạn muốn đổi từ SendGrid sang provider khác, phần lớn ứng dụng không cần thay đổi gì cả.


Yêu Cầu

  • Linux server (Ubuntu/Debian) đã cài Docker
  • Tài khoản SendGrid với API key có quyền Mail Send
  • Verified sender identity hoặc domain trong SendGrid
  • Kiến thức cơ bản về Docker và Docker Compose

Nếu bạn không dùng SendGrid, phần kiến trúc vẫn giữ nguyên. Chỉ phần credential và upstream relay host sẽ thay đổi theo provider.


Bước 1: Tạo External Docker Network

Đầu tiên, tạo một fixed external Docker network. Network này tồn tại độc lập với mọi chu kỳ docker compose up/down, nên tất cả container join vào đều luôn reach được nhau.

docker network create --driver bridge smtp-network

Tại sao external? Vì:

  • Nó survive khi container restart và redeploy
  • Nhiều Compose project độc lập có thể join cùng network
  • Bạn control tên network — không có random prefix

Kiểm tra network đã tạo:

docker network ls | grep smtp-network

Bước 2: Build SMTP Relay Container

Chúng ta sẽ dùng Postfix làm SMTP relay — battle-tested, nhẹ, và hoàn hảo cho việc relay mail.

Cấu Trúc Project

smtp-relay/
├── Dockerfile
├── docker-compose.yml
├── .env
└── entrypoint.sh

Dockerfile

FROM ubuntu:24.04
 
ENV DEBIAN_FRONTEND=noninteractive
 
RUN apt-get update && \
    apt-get install -y postfix libsasl2-modules ca-certificates && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
 
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
 
EXPOSE 25
 
CMD ["/entrypoint.sh"]

entrypoint.sh

Trong phiên bản ban đầu, script này configure Postfix để relay qua SendGrid lúc container startup bằng environment variable:

#!/bin/bash
set -e
 
# Configure Postfix làm SendGrid relay
postconf -e "relayhost = [smtp.sendgrid.net]:587"
postconf -e "smtp_sasl_auth_enable = yes"
postconf -e "smtp_sasl_password_maps = static:apikey:${SENDGRID_API_KEY}"
postconf -e "smtp_sasl_security_options = noanonymous"
postconf -e "smtp_tls_security_level = encrypt"
postconf -e "smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt"
postconf -e "header_size_limit = 4096000"
 
# Chỉ accept mail từ Docker network
postconf -e "mynetworks = 127.0.0.0/8 172.16.0.0/12 10.0.0.0/8 192.168.0.0/16"
postconf -e "inet_interfaces = all"
postconf -e "inet_protocols = ipv4"
 
# Set hostname
postconf -e "myhostname = ${MAIL_HOSTNAME:-smtp-relay.local}"
postconf -e "mydomain = ${MAIL_HOSTNAME:-smtp-relay.local}"
 
# Tắt local delivery — relay tất cả
postconf -e "mydestination = "
postconf -e "local_transport = error:no local delivery"
 
echo "Starting Postfix SMTP relay..."
echo "Relay host: smtp.sendgrid.net:587"
echo "Hostname: ${MAIL_HOSTNAME:-smtp-relay.local}"
 
# Start Postfix ở foreground
postfix start-fg

Giải thích các setting quan trọng:

SettingMục đích
relayhostForward tất cả mail tới SendGrid SMTP server trên port 587
smtp_sasl_auth_enableBật authentication với SendGrid
smtp_sasl_password_mapsSendGrid dùng apikey làm username và API key làm password
smtp_tls_security_levelBắt buộc TLS encryption tới SendGrid
mynetworksChỉ accept mail từ private IP range (Docker network)
mydestinationĐể trống — không deliver local, relay tất cả
inet_interfaces = allListen trên tất cả interface trong container

.env

SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
MAIL_HOSTNAME=smtp-relay.local

Không bao giờ commit file .env lên version control. Thêm nó vào .gitignore.

docker-compose.yml

services:
  smtp-relay:
    build: .
    container_name: smtp-relay
    restart: unless-stopped
    env_file:
      - .env
    networks:
      - smtp-network
 
networks:
  smtp-network:
    external: true

Lưu ý:

  • Không map port ra host (ports cố tình bỏ qua) — relay chỉ accessible từ container khác trên smtp-network, không từ internet
  • Network khai báo external: true — tham chiếu tới network đã tạo ở Bước 1

Build và Chạy

cd smtp-relay
docker compose up -d --build

Kiểm tra relay đang chạy:

docker compose logs smtp-relay

Bạn sẽ thấy:

Starting Postfix SMTP relay...
Relay host: smtp.sendgrid.net:587
Hostname: smtp-relay.local
postfix/master[1]: daemon started -- version 3.8.x

Mở Rộng: Hỗ Trợ Nhiều Provider Qua .env

Đây là điểm mới trong README hiện tại của project. Thay vì relay cố định qua SendGrid, bạn có thể dùng biến MAIL_PROVIDER để chuyển giữa nhiều upstream provider mà không phải thay đổi cách app connect tới relay.

VariableDescription
MAIL_PROVIDERsendgrid (default), hostinger, hoặc resend
MAIL_HOSTNAMEPostfix hostname (default: smtp-relay.local)
SENDGRID_API_KEYSendGrid API key khi dùng SendGrid
HOSTINGER_EMAILEmail account dùng với Hostinger
HOSTINGER_PASSWORDPassword email Hostinger
RESEND_API_KEYResend API key

Ví dụ cấu hình:

MAIL_PROVIDER=sendgrid
MAIL_HOSTNAME=smtp-relay.local
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
MAIL_PROVIDER=hostinger
MAIL_HOSTNAME=smtp-relay.local
HOSTINGER_EMAIL=[email protected]
HOSTINGER_PASSWORD=your-email-password
MAIL_PROVIDER=resend
MAIL_HOSTNAME=smtp-relay.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx

Sau khi đổi .env, rebuild lại container:

docker compose up -d --build

Điều hay ở đây là các app phía sau vẫn giữ nguyên:

  • vẫn gửi tới smtp-relay
  • vẫn dùng port 25
  • vẫn không cần auth hoặc TLS ở phía application container

Toàn bộ khác biệt nằm ở relay layer.


Bước 3: Connect App Container

Bất kỳ Docker Compose project nào giờ đều có thể gửi email qua relay bằng cách join smtp-network.

Ví dụ: Web App Gửi Email

# Trong docker-compose.yml của app
services:
  web-app:
    image: your-app:latest
    environment:
      SMTP_HOST: smtp-relay
      SMTP_PORT: 25
      SMTP_TLS: "false"        # TLS do relay handle -> provider upstream
      SMTP_AUTH: "false"       # Không cần auth cho local relay
    networks:
      - default
      - smtp-network
 
networks:
  smtp-network:
    external: true

App connect tới smtp-relay:25 — container name được resolve qua Docker DNS vì cả hai container cùng nằm trên smtp-network.

Đây cũng là lý do mô hình này khá bền theo thời gian: bạn có thể chuyển SendGrid sang Hostinger hoặc Resend mà không cần sửa cấu hình SMTP trong từng app.

Ví dụ: Django Settings

# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp-relay'
EMAIL_PORT = 25
EMAIL_USE_TLS = False
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
DEFAULT_FROM_EMAIL = '[email protected]'

Ví dụ: Node.js với Nodemailer

const nodemailer = require('nodemailer');
 
const transporter = nodemailer.createTransport({
  host: 'smtp-relay',
  port: 25,
  secure: false,
  tls: {
    rejectUnauthorized: false
  }
});
 
await transporter.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Hello from Docker',
  text: 'Email này được gửi qua local SMTP relay!'
});

Ví dụ: Spring Boot (application.yml)

spring:
  mail:
    host: smtp-relay
    port: 25
    properties:
      mail.smtp.auth: false
      mail.smtp.starttls.enable: false

Ví dụ: Test bằng curl

Từ bên trong bất kỳ container nào trên smtp-network:

# Test nhanh với curl
curl --url 'smtp://smtp-relay:25' \
  --mail-from '[email protected]' \
  --mail-rcpt '[email protected]' \
  --upload-file - <<EOF
From: [email protected]
To: [email protected]
Subject: SMTP Relay Test
 
Đây là email test gửi qua local SMTP relay.
EOF

Bước 4: Kiểm Tra Email Delivery

Check Postfix Log

docker logs smtp-relay 2>&1 | tail -20

Delivery thành công trông như này:

postfix/smtp[123]: ABC123DEF: to=<[email protected]>,
  relay=smtp.sendgrid.net[xxx.xxx.xxx.xxx]:587,
  status=sent (250 Ok queued as xxxxxxxxx)

Nếu bạn dùng provider khác, hostname ở dòng relay= sẽ thay đổi, nhưng pattern debug nhìn chung vẫn giống nhau.

Check Provider Dashboard

Với SendGrid, bạn có thể vào SendGrid → Activity để xem email đã gửi, delivery status, open, và click.

Với Hostinger hoặc Resend, hãy kiểm tra dashboard tương ứng của provider bạn đang dùng. Ý chính ở đây là relay vẫn là một lớp trung gian; trạng thái delivery cuối cùng thường sẽ hiện cả ở log Postfix lẫn dashboard của provider.

Check Mail Queue

Nếu email bị stuck:

# Xem mail queue
docker exec smtp-relay postqueue -p
 
# Flush queue (retry delivery)
docker exec smtp-relay postqueue -f
 
# Xem lý do deferred mail
docker exec smtp-relay postcat -q <QUEUE_ID>

Tổng Quan Kiến Trúc

Đây là bức tranh toàn cảnh cách mọi thứ connect với nhau:

Điểm quan trọng:

  • Tất cả container share smtp-network bridge
  • Relay listen trên port 25 chỉ trong network — không expose ra host hay internet
  • Relay authenticate tới provider upstream qua TLS
  • App không cần giữ credential của SendGrid, Hostinger, hay Resend — chỉ gửi tới smtp-relay:25

Security Best Practice

1. Không Expose Port 25 Ra Host

Không bao giờ thêm ports: "25:25" vào Compose file của relay. Relay chỉ nên reachable từ Docker network.

# ❌ SAI — expose SMTP ra toàn bộ host network
services:
  smtp-relay:
    ports:
      - "25:25"
 
# ✅ ĐÚNG — chỉ accessible từ smtp-network
services:
  smtp-relay:
    networks:
      - smtp-network

2. Giới Hạn mynetworks

Setting mynetworks của Postfix kiểm soát ai được gửi mail qua relay. Config của chúng ta giới hạn ở private IP range:

mynetworks = 127.0.0.0/8 172.16.0.0/12 10.0.0.0/8 192.168.0.0/16

Để security chặt hơn, chỉ dùng subnet cụ thể của Docker network:

# Tìm subnet của smtp-network
docker network inspect smtp-network --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
 
# Rồi dùng subnet cụ thể đó trong entrypoint.sh
postconf -e "mynetworks = 127.0.0.0/8 172.20.0.0/16"

3. Rotate Credential Định Kỳ

Lưu key hoặc password trong .env và rotate định kỳ. Nếu bị lộ, revoke hoặc reset ngay trên dashboard của provider đang dùng.

Với SendGrid, tốt nhất là tạo API key riêng chỉ với quyền Mail Send. Đừng dùng lại key có full account access.

4. Monitor Mail Queue

Setup alert nếu mail queue tăng bất thường — có thể là SendGrid credential issue, Hostinger auth fail, Resend API key sai, rate limiting, hoặc container bị compromise đang spam qua relay.

# Check đơn giản queue size
QUEUE_SIZE=$(docker exec smtp-relay postqueue -p | tail -1 | grep -oP '\d+(?= Request)')
if [ "$QUEUE_SIZE" -gt 100 ]; then
  echo "Warning: SMTP relay queue có $QUEUE_SIZE message"
fi

5. Giữ Credential Chỉ Ở Relay Layer

Càng ít container biết credential thật sự, bề mặt tấn công càng nhỏ. Đây là một lợi ích bảo mật khá thực tế của SMTP relay nội bộ.


Xử Lý Sự Cố

"Connection refused" từ App Container

Nguyên nhân: App container không nằm trên smtp-network.

Cách fix: Thêm network vào docker-compose.yml của app:

networks:
  smtp-network:
    external: true

Và attach service vào network đó.

"Relay access denied" (Postfix Error 554)

Nguyên nhân: IP của container gửi không nằm trong mynetworks.

Cách fix: Check IP của container và đảm bảo nó nằm trong mynetworks range cho phép:

docker inspect <container_name> --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

Email Bị Stuck Trong Queue

Nguyên nhân: Authentication fail, sender chưa verify, hoặc provider đang rate limit.

Cách fix:

# Check log để tìm error cụ thể
docker logs smtp-relay 2>&1 | grep "status=deferred"
 
# Các vấn đề thường gặp:
# - Invalid API key → tạo lại trong dashboard provider
# - Unverified sender → verify domain/email
# - Rate limit → check quota hoặc plan limit

"TLS required but not available"

Nguyên nhân: Thiếu CA certificate trong container.

Cách fix: Đảm bảo ca-certificates đã được install trong Dockerfile (đã có sẵn trong setup của chúng ta).


Bonus: Health Check

Thêm health check cho relay container để Docker tự restart nếu Postfix crash:

services:
  smtp-relay:
    build: .
    container_name: smtp-relay
    restart: unless-stopped
    env_file:
      - .env
    networks:
      - smtp-network
    healthcheck:
      test: ["CMD", "postfix", "status"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

Tổng Kết

Setup local SMTP relay với SendGrid vẫn là ví dụ dễ hiểu và thực dụng nhất:

  • Centralized email delivery — một nơi duy nhất manage credential và monitor delivery
  • Simple app configuration — chỉ cần trỏ tới smtp-relay:25, không cần SDK
  • Network isolation — relay chỉ accessible từ Docker container, không từ internet
  • Reliable delivery — Postfix handle queuing và retry

Điểm mở rộng quan trọng trong phiên bản mới là relay không còn bị khóa cứng vào SendGrid. Bạn có thể giữ nguyên mô hình này nhưng đổi upstream sang Hostinger hoặc Resend chỉ bằng cách cập nhật .env và rebuild container.

Pattern này rất phù hợp cho self-hosted setup nơi nhiều service cần gửi email mà không cần mỗi cái tự manage provider integration riêng.


Bài Viết Liên Quan

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.