How to Set Up Postal v3 on AWS for Production: The Ultimate Enterprise Guide
Quick summary: Postal v3 on AWS: 2 vCPU / 4 GiB minimum per official docs, RDS MariaDB production path, $250-$700/mo dev footprint, Docker + Caddy architecture, and 100+ item readiness checklist.
Key Takeaways
- Postal v3 on AWS: 2 vCPU / 4 GiB minimum per official docs, RDS MariaDB production path, $250-$700/mo dev footprint, Docker + Caddy architecture, and 100+ item readiness checklist
- Postal v3's official prerequisites specify a dedicated server with at least 2 CPU cores, 4 GiB RAM, and 25 GiB disk, running Docker and MariaDB 10
- 6+
- AWS documents the same EC2 port-25 restriction that affects every self-hosted mail platform
- As of July 2026, Postal v3 runs as containerized web, SMTP, and worker services via the postalserver/install helper — not as a single monolithic daemon

Table of Contents
Postal v3’s official prerequisites specify a dedicated server with at least 2 CPU cores, 4 GiB RAM, and 25 GiB disk, running Docker and MariaDB 10.6+. AWS documents the same EC2 port-25 restriction that affects every self-hosted mail platform. As of July 2026, Postal v3 runs as containerized web, SMTP, and worker services via the postalserver/install helper — not as a single monolithic daemon.
This guide is for DevOps engineers, platform teams, and deliverability specialists deploying Postal v3 on AWS. For managed paths, see Amazon SES consulting and SES migration. Comparing bare MTA options? Read our KumoMTA on AWS guide.
Artifacts: README, postal.yml example, Route53 DNS template, CloudWatch agent config, readiness checklist.
Benchmark pattern (not a cited client) — B2B SaaS notification platform, 50k-200k emails/day,
t3.xlargeEC2 + RDSdb.t4g.mediumMariaDB, HTTP API primary send path. Development AWS footprint: $250-$700/month (compute + RDS + observability in this guide’s cost table).
What broke — A team used the dev
docker run mariadbexample from Postal docs with the default password andinnodb_log_file_sizeleft at RDS defaults. Messages above 8 MB failed silently in the worker queue until log inspection revealed InnoDB row-size errors. Fix: RDS parameter group with 160 MBinnodb_log_file_sizefor a 15 MB max message policy.
Reproduce this — Copy templates from
examples/postal-aws-production/. Runpostal bootstrapon a non-production hostname before publishing customer DNS.
Executive Summary
Postal v3 on AWS is a containerized mail delivery platform — web UI, HTTP API, SMTP server, and background workers — backed by MariaDB. It fits teams that want self-hosted email with a management interface, not teams that only need a high-throughput MTA engine.
| Question | Short answer |
|---|---|
| Who needs Postal on AWS? | SaaS and agencies sending 50k+ emails/day who want UI-managed virtual mail servers and HTTP API injection |
| Who should skip? | Teams without deliverability ops capacity, or senders better served by Amazon SES |
| Minimum shape? | 2 vCPU / 4 GiB per Postal docs; production baseline 4 vCPU / 16 GiB + RDS MariaDB |
| Biggest architecture mistake? | Putting SMTP behind ALB/CloudFront — Postal v3 uses network_mode: host for SMTP and web processes |
| Our recommendation | Use Postal for UI + API + multi-mail-server management; use KumoMTA for policy-heavy high-volume MTA; use SES when managed deliverability wins |
What Is Postal v3?
Postal is an open-source mail delivery platform — not just an SMTP relay. Postal v3 packages:
| Component | Role | Production note |
|---|---|---|
| Web server | Admin UI, HTTP API | Bind behind Caddy TLS on postal.example.com |
| SMTP server | Inbound/outbound SMTP | network_mode: host; requires Elastic IP + PTR |
| Worker | Queue processing, delivery attempts | Scale vertically before adding second host |
| MariaDB | Config + message metadata | Use RDS Multi-AZ for production |
| Caddy | TLS termination (default install path) | --network host per installation docs |
Virtual mail servers
Each mail server in Postal is a logical sending entity with its own domains, credentials, IP pools, and statistics. This maps well to SaaS products that send on behalf of multiple customers — but reputation isolation still requires disciplined IP and domain strategy.
Functional model
flowchart LR
app[SendingApp] --> api[PostalHTTPAPI]
app --> smtp587[SMTPSubmission]
api --> worker[PostalWorker]
smtp587 --> worker
worker --> smtp25[OutboundSMTP]
smtp25 --> mx[RecipientMX]
worker --> db[(MariaDBRDS)]
worker --> webhooks[WebhooksAndEvents]Why Choose Postal v3?
| Advantage | Why it matters on AWS |
|---|---|
| Web UI + API | Faster onboarding than raw MTA configuration |
| Docker-native | postal start orchestrates web/smtp/worker containers |
| Virtual mail servers | Multi-tenant sending from one platform install |
| Open source | No per-message license; you pay AWS + operations time |
| IP pools | postal.use_ip_pools for stream isolation when configured |
Feature comparison
| Platform | UI | HTTP API | SMTP | Multi-tenant | High-volume policy | Ops burden |
|---|---|---|---|---|---|---|
| Postal v3 | Strong | Yes | Yes | Virtual mail servers | Moderate | Medium-high |
| KumoMTA | Minimal | Policy hooks | Yes | Via policy | Strong | High |
| Amazon SES | AWS Console | Yes | Yes | Tenants (2025+) | Managed | Low-medium |
| Mailgun/SendGrid | SaaS | Yes | Yes | Accounts | Managed | Low |
| Postfix | None | No | Yes | Custom | Custom | Medium |
Recommendation matrix
| Scenario | Recommended path |
|---|---|
| SMB/mid-market self-hosted with UI + API | Postal v3 |
| Engineering-led 1M+ emails/day, Lua shaping | KumoMTA |
| AWS-native managed deliverability | Amazon SES |
| Quick ESP migration | SES migration |
AWS Production Architecture
CloudFront, WAF, and ALB do not terminate Postal SMTP traffic. Postal v3 containers use network_mode: host for web and SMTP processes (docker-compose.v3.yml). HTTPS is handled by Caddy in the default install path.
flowchart TD
internet[Internet] --> r53[Route53]
app[SendingApplications] --> api[PostalHTTPSAPI]
app --> smtp587[PostalSMTP587]
api --> ec2[EC2UbuntuDockerHost]
smtp587 --> ec2
ec2 --> eip[ElasticIP]
eip --> mx[RecipientMX]
ec2 --> caddy[CaddyHostNetworkTLS]
ec2 --> rds[RDSMariaDB106]
subgraph support [AWSSupportingServices]
cw[CloudWatch]
ssm[SystemsManager]
sm[SecretsManager]
s3[S3Backups]
gd[GuardDuty]
end
ec2 --> cw
ec2 --> ssm
rds --> smWhy each AWS service exists
| Service | Role |
|---|---|
| EC2 | Docker host for Postal web, smtp, worker, Caddy |
| Elastic IP | Stable SMTP/IP reputation identity |
| RDS MariaDB 10.6+ | Production database (replaces dev Docker MariaDB) |
| Route53 | Postal DNS spec (spf, return-path, route, track domains) |
| Secrets Manager | DB passwords, API secrets |
| CloudWatch | Metrics, logs, alarms |
| Systems Manager | Patch and session access without public SSH |
| S3 | Config and backup archive |
| GuardDuty / Inspector | Threat and vulnerability visibility |
Infrastructure Design
flowchart LR
igw[InternetGateway] --> pub[PublicSubnet]
pub --> ec2[PostalEC2]
pub --> eip[EIP]
priv[PrivateSubnet] --> rds[RDSMariaDB]
ec2 --> rdsVPC baseline
| Component | Example | Notes |
|---|---|---|
| VPC | 10.50.0.0/16 | Room for HA expansion |
| Public subnet | 10.50.10.0/24 | EC2 + Elastic IP |
| Private subnet A/B | 10.50.20.0/24, 10.50.21.0/24 | RDS subnet group |
| Security group (EC2) | In: 80,443,25,587 from required sources; Out: all for SMTP delivery | Restrict 22; use SSM |
| Security group (RDS) | In: 3306 from EC2 SG only | No public RDS |
Reverse DNS
Request PTR for each Elastic IP so postal.example.com forward and reverse DNS align — same requirement as any production MTA.
Server Sizing
Postal minimum: 2 vCPU, 4 GiB RAM, 25 GiB disk. Production baselines below account for Docker overhead, MariaDB client load, and queue spikes.
| Tier | EC2 | RDS | Est. emails/day | Notes |
|---|---|---|---|---|
| Development | t3.medium | db.t4g.micro | < 10k | Lab only; not for production mail |
| Startup | t3.xlarge | db.t4g.medium | 50k-200k | Single AZ acceptable initially |
| Medium | m7i.xlarge | db.r7g.large | 200k-1M | Enable RDS Multi-AZ |
| Enterprise | m7i.2xlarge+ multi-node | db.r7g.xlarge Multi-AZ | 1M+ | Active-passive Postal hosts + shared RDS |
Operating System
Use Ubuntu 22.04 LTS — the official v3 prerequisite script is install-ubuntu.v3.sh (install repo). Install Docker Engine and Docker Compose plugin per Docker docs before Postal bootstrap.
Step-by-Step Installation on AWS
Context: Ubuntu 22.04 LTS EC2, Elastic IP attached, Route53 hostname ready, RDS MariaDB 10.6+ provisioned in private subnets.
0) Provision RDS MariaDB first
Create an RDS MariaDB 10.6+ instance in private subnets before postal initialize — Postal needs credentials that can create and manage postal and postal-* message databases (prerequisites).
| Setting | Production value |
|---|---|
| Engine | MariaDB 10.6.20+ or 10.11 |
| Instance class | db.t4g.medium minimum for startup |
| Storage | gp3, autoscaling enabled |
| Multi-AZ | Yes (production) |
| Parameter group | innodb_log_file_size per max message size |
| Public access | No |
| Security group | Inbound 3306 from Postal EC2 SG only |
Store the master password in Secrets Manager; reference it when editing postal.yml — never commit credentials to git.
1) System packages
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl jq2) Docker (official)
# Context: Docker Engine + Compose plugin — see https://docs.docker.com/engine/install/ubuntu/
curl -fsSL https://get.docker.com | sudo sh
sudo docker version && sudo docker compose version3) Clone Postal v3 install helper
sudo git clone https://github.com/postalserver/install /opt/postal/install
sudo ln -sf /opt/postal/install/bin/postal /usr/bin/postal4) Bootstrap configuration
# Context: replace postal.example.com with your Route53 hostname
sudo postal bootstrap postal.example.comThis generates /opt/postal/config/postal.yml, signing.key, and Caddyfile.
5) Configure RDS in postal.yml
Edit /opt/postal/config/postal.yml — use v2 format (version: 2). Point main_db and message_db at RDS. See postal.yml.example.
main_db:
host: postal-db.xxxx.us-east-1.rds.amazonaws.com
username: postal_admin
password: '<from-secrets-manager>'
database: postal6) Initialize database and admin user
sudo postal initialize
sudo postal make-user7) Start Postal
sudo postal start
sudo postal status8) Start Caddy for TLS
# Context: from Postal installation docs — Caddy uses host network
sudo docker run -d \
--name postal-caddy \
--restart always \
--network host \
-v /opt/postal/config/Caddyfile:/etc/caddy/Caddyfile \
-v /opt/postal/caddy-data:/data \
caddy9) Verify
- Browse
https://postal.example.comand log in - Create first virtual mail server in UI
- Send test via API (see below)
Configure Postal for Production
DNS block in postal.yml
dns:
mx_records:
- mx1.postal.example.com
- mx2.postal.example.com
spf_include: spf.postal.example.com
return_path_domain: rp.postal.example.com
route_domain: routes.postal.example.com
track_domain: track.postal.example.comIP pools
Enable postal.use_ip_pools: true and assign pools per mail server in the UI. Separate transactional and marketing streams onto different pools before scaling volume.
HTTP API send example
Context: API key from Postal mail server credentials page
curl --location 'https://postal.example.com/api/v1/send/message' \
--header 'X-Server-API-Key: YOUR_SERVER_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"to": ["user@example.com"],
"from": "notifications@yourdomain.com",
"subject": "Test",
"plain_body": "Postal v3 on AWS test"
}'RDS parameter: innodb_log_file_size
Postal requires innodb_log_file_size ≥ 10× max message size. For 15 MB messages, set 160 MB or higher in your RDS parameter group before production traffic.
Route53 DNS Configuration
Publish records from route53-records.example.txt:
| Record | Purpose |
|---|---|
postal.example.com A | Web, API, SMTP host |
spf.postal.example.com TXT | Global SPF include for server IPs |
rp.postal.example.com | Return-path / MAIL FROM domain |
routes.postal.example.com MX | Inbound route handling |
track.postal.example.com A | Click/open tracking (optional) |
| Customer domain SPF | include:spf.postal.example.com |
| DMARC on customer domains | Start p=none, tighten after monitoring |
Run postal default-dkim-record for DKIM TXT on the return-path domain.
Security Best Practices
- SSM Session Manager instead of public SSH
- Secrets Manager for RDS passwords — inject at boot via user-data or SSM Parameter Store
- IAM instance role least privilege: CloudWatch, S3 backup bucket, Secrets read
- Security groups — no 3306 from internet; restrict SMTP submission sources
- signing.key permissions
600, root-owned under/opt/postal/config - Patch cadence — Ubuntu unattended upgrades + Docker image updates via
postal upgradeworkflow - GuardDuty + Inspector on EC2
SSH hardening
# Context: /etc/ssh/sshd_config on Postal EC2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yesMonitoring Architecture
flowchart LR
ec2[PostalEC2] --> cwa[CloudWatchAgent]
cwa --> cwm[Metrics]
cwa --> cwl[Logs]
cwm --> alarms[Alarms]
alarms --> sns[SNS]
rds[RDS] --> rdsi[RDSMetrics]
rdsi --> alarmsPriority alarms
| Signal | Threshold idea |
|---|---|
| EC2 CPU | > 80% for 15 min |
| Disk usage | > 85% |
| RDS CPU / connections | Sustained high |
| SMTP 4xx/5xx rate | Spike vs baseline |
| API 5xx from Caddy | Any sustained |
Deploy cloudwatch-agent.json on the EC2 host.
Logging Pipeline
flowchart TD
docker[DockerContainers] --> syslog[HostSyslog]
syslog --> cwl[CloudWatchLogs]
cwl --> s3[S3Archive]
s3 --> athena[Athena]- Postal
logging.stdout: truein config for container log capture - Retention: 14-30 days CloudWatch hot, 90-365 days S3 cold
Logrotate on EC2
# Context: /etc/logrotate.d/postal-host — host-level logs alongside Docker
/var/log/syslog {
daily
rotate 14
compress
delaycompress
missingok
notifempty
}Docker container logs under /var/lib/docker/containers/ grow quickly under high volume — monitor disk and set CloudWatch agent inclusion carefully to control ingest cost.
Email Deliverability
flowchart LR
dns[DNSAuth] --> warmup[IPWarmup]
warmup --> send[ConservativeVolume]
send --> monitor[PostmasterAndComplaints]
monitor --> tune[PoolAndCadenceTuning]- Warm up each new Elastic IP gradually
- Keep transactional and marketing on separate IP pools
- Monitor Google Postmaster, Microsoft SNDS, complaint rates in Postal UI
- Enforce SPF, DKIM (Postal-generated), DMARC on customer domains
- List-Unsubscribe headers on bulk streams
High Availability
| Pattern | Description |
|---|---|
| Single node | Default; acceptable for startup with RDS Multi-AZ |
| Active-passive | Second EC2 with shared RDS; manual or DNS failover |
| RDS Multi-AZ | Database failover without Postal config loss |
Backup /opt/postal/config (including signing.key) to S3 versioned bucket daily.
flowchart LR
fail[NodeFailure] --> dns[UpdateDNSorEIP]
dns --> standby[StandbyPostalHost]
standby --> rds[RDSMultiAZ]
standby --> restore[RestoreConfigFromS3]Performance Tuning
EC2 and Docker
- EBS gp3 with provisioned IOPS for
/var/lib/docker - Adequate inode headroom on Docker volume
postal status— ensure worker container healthy under load
sysctl baseline
# Context: /etc/sysctl.d/99-postal.conf
net.core.somaxconn = 4096
net.ipv4.ip_local_port_range = 10240 65535
fs.file-max = 2097152Cost Optimization
| Profile | Approx monthly total |
|---|---|
| Development | $250-$700 |
| Startup production | $900-$3,000 |
| Medium scale | $3,500-$12,000 |
| Enterprise | $15,000+ |
Savings: RDS Reserved Instances, Graviton (t4g/m7g) where tested, S3 lifecycle for logs, right-size EC2 after 30-day baseline.
Troubleshooting
| Issue | Likely cause | Fix |
|---|---|---|
postal initialize DB error | RDS SG or credentials | Open 3306 from EC2 SG; verify Secrets Manager values |
| Caddy cert failure | DNS not pointing to EIP | Fix Route53 A record; wait for propagation |
| API 401 | Wrong API key | Regenerate credential in mail server UI |
| SMTP timeout outbound | Port 25 blocked | AWS support request to remove restriction |
| Large message failures | innodb_log_file_size | Increase RDS parameter per Postal docs |
| Worker queue stuck | Worker container down | postal status; postal restart |
| Spam folder placement | Warmup/reputation | Reduce volume; check Postmaster |
sequenceDiagram
participant App
participant Postal as PostalAPI
participant Worker
participant MX as RecipientMX
App->>Postal: POST /api/v1/send/message
Postal->>Worker: Queue message
Worker->>MX: SMTP delivery
MX-->>Worker: 250/4xx/5xx
Worker-->>App: Webhook eventMaintenance Cadence
| Cadence | Task |
|---|---|
| Daily | postal status, queue depth, bounce/complaint review |
| Weekly | DNS spot-check, cert expiry, RDS storage |
| Monthly | Patch OS, review RDS performance insights |
| Quarterly | DR restore drill, config backup test |
Do’s and Don’ts
Do’s (30)
| # | Do | Why |
|---|---|---|
| 1 | Use RDS MariaDB for production | Secure, backed-up database |
| 2 | Run Postal on a dedicated EC2 instance | Postal docs requirement |
| 3 | Request AWS port 25 removal if needed | Direct MX delivery |
| 4 | Align PTR with postal hostname | Deliverability trust |
| 5 | Publish full Postal DNS set | SPF include, return-path, routes |
| 6 | Use IP pools per stream | Reputation isolation |
| 7 | Store secrets in Secrets Manager | No plaintext in git |
| 8 | Enable RDS Multi-AZ for production | DB resilience |
| 9 | Size innodb_log_file_size correctly | Large message support |
| 10 | Use SSM for access | Reduce SSH attack surface |
| 11 | Monitor worker container health | Queue processing depends on it |
| 12 | Backup signing.key securely | Signing integrity |
| 13 | Start DMARC at p=none | Visibility before enforcement |
| 14 | Warm up new IPs gradually | Avoid ISP throttling |
| 15 | Test API sends before SMTP cutover | Faster debugging |
| 16 | Tag AWS resources | Cost allocation |
| 17 | Enable VPC flow logs | Network debugging |
| 18 | Rotate API keys periodically | Credential hygiene |
| 19 | Document virtual mail server ownership | Multi-tenant clarity |
| 20 | Use CloudWatch alarms | Early incident detection |
| 21 | Keep Docker images updated | Security patches |
| 22 | Restrict SMTP submission by SG | Abuse prevention |
| 23 | Validate Caddy TLS renewal | Web UI availability |
| 24 | Archive logs to S3 | Cost-effective retention |
| 25 | Run quarterly DR drills | Prove restore path |
| 26 | Separate marketing and transactional pools | Protect OTP mail |
| 27 | Verify customer domain DNS in UI | Per-domain authentication |
| 28 | Use Elastic IP consistently | Stable reputation |
| 29 | Review Postal upgrade notes before upgrading | v3 breaking changes |
| 30 | Compare TCO vs SES at your volume | Economic sanity check |
Don’ts (30)
| # | Don’t | Why |
|---|---|---|
| 1 | Don’t use install-ubuntu.v3.sh passwords in prod | Insecure defaults |
| 2 | Don’t put SMTP behind ALB | Wrong layer for SMTP |
| 3 | Don’t expose RDS publicly | Attack surface |
| 4 | Don’t skip PTR setup | Provider trust failures |
| 5 | Don’t share one IP pool for all streams | Reputation coupling |
| 6 | Don’t run other heavy workloads on Postal EC2 | Resource contention |
| 7 | Don’t ignore worker container crashes | Silent queue stall |
| 8 | Don’t commit signing.key to git | Key compromise |
| 9 | Don’t ramp volume without warmup | Blocking risk |
| 10 | Don’t use MySQL instead of MariaDB | Unsupported by Postal |
| 11 | Don’t mix v2 Postal install paths | v3 only in this guide |
| 12 | Don’t leave default RDS parameters for large mail | Message size failures |
| 13 | Don’t open SSH to 0.0.0.0/0 | Brute force risk |
| 14 | Don’t skip DMARC monitoring | Blind spoofing visibility |
| 15 | Don’t delete mail server DBs manually | Data loss |
| 16 | Don’t run without backups | No restore path |
| 17 | Don’t ignore complaint spikes | Reputation damage |
| 18 | Don’t use self-signed certs in production | Client trust issues |
| 19 | Don’t overload track domain without testing | Broken click tracking |
| 20 | Don’t assume API covers all admin tasks | Some functions UI-only today |
| 21 | Don’t scale EC2 before fixing DB bottlenecks | Wasted spend |
| 22 | Don’t forget to restart after config changes | Postal docs requirement |
| 23 | Don’t use single-AZ RDS for production SLA | DB outage = full stop |
| 24 | Don’t send marketing from transactional pool | OTP placement risk |
| 25 | Don’t ignore Docker disk growth | Full disk stops containers |
| 26 | Don’t skip synthetic monitoring | Late outage detection |
| 27 | Don’t change SPF without testing | Auth failures |
| 28 | Don’t run production without alarms | Operational blindness |
| 29 | Don’t treat Postal as zero-ops ESP | You own deliverability |
| 30 | Don’t block port 25 testing in staging only | Production surprise |
Common Real-World Mistakes (25)
| # | Problem | Impact | Solution |
|---|---|---|---|
| 1 | Dev MariaDB in production | Security + scale limits | RDS MariaDB 10.6+ |
| 2 | Missing innodb_log_file_size | Large sends fail | RDS parameter group |
| 3 | No PTR on Elastic IP | Deferrals/rejections | AWS PTR request |
| 4 | Wrong postal.yml paths | Container mount errors | Use /config paths in containers |
| 5 | Forgot postal restart after config | Stale behavior | Restart per docs |
| 6 | Caddy not on host network | TLS/route failures | --network host |
| 7 | API key on wrong mail server | 401 errors | Match server credential |
| 8 | Marketing on transactional IP | OTP spam-folder | IP pool split |
| 9 | No RDS backups | Data loss risk | Enable automated backup |
| 10 | Port 25 still blocked | SMTP timeouts | AWS support ticket |
| 11 | Undersized disk for Docker | Container exit | Monitor disk/inodes |
| 12 | Missing return-path DNS | Bounce handling breaks | Publish rp.* records |
| 13 | No worker health check | Queue backlog | Monitor postal status |
| 14 | Shared signing.key across envs | Cross-env forgery risk | Per-environment keys |
| 15 | Skipping postal initialize | Schema missing | Run before start |
| 16 | Public admin without WAF | Abuse scans | Restrict or add WAF on 443 |
| 17 | No customer DMARC | Spoofing visibility gap | Publish _dmarc |
| 18 | Tracking domain without A record | Broken links | track.* A record |
| 19 | Single admin user | No break-glass backup | Second admin account |
| 20 | Ignoring Postal upgrade docs | Breaking upgrades | Read release notes |
| 21 | RDS in wrong subnet | Connection timeout | Private subnet + SG |
| 22 | No log retention policy | Cost + compliance gap | Tiered retention |
| 23 | Testing only via API | SMTP path untested | Test both paths |
| 24 | No complaint workflow | Reputation decay | Weekly review cadence |
| 25 | Choosing Postal for 10M+/day without MTA plan | Cost/complexity | Evaluate KumoMTA or SES |
Production Readiness Checklist (100+)
Infrastructure
- Dedicated EC2 provisioned
- Elastic IP attached
- Ubuntu 22.04 LTS hardened
- Docker and Compose installed
- EBS gp3 sized for Docker growth
- VPC flow logs enabled
- NAT not required for public-subnet EC2 SMTP path documented
Database
- RDS MariaDB 10.6+ provisioned
- Multi-AZ enabled (production)
- Parameter group: innodb_log_file_size sized
- Automated backups configured
- SG allows 3306 from EC2 only
Postal platform
- install helper cloned to /opt/postal/install
- postal bootstrap completed
- postal.yml v2 format with RDS hosts
- postal initialize successful
- Admin user created
- postal start — all containers up
- Caddy TLS valid
- First virtual mail server created
DNS
- postal.example.com A record
- PTR aligned
- spf.postal.example.com TXT
- rp.postal.example.com records + DKIM
- routes.postal.example.com MX
- track.postal.example.com (if used)
- Customer domain SPF includes
Security
- Secrets Manager for DB passwords
- SSM Session Manager enabled
- SSH restricted
- signing.key permissions verified
- IAM least privilege
Deliverability
- IP warmup plan documented
- IP pools configured
- DMARC on sending domains
- Postmaster tools access
- Complaint threshold defined
Monitoring
- CloudWatch agent deployed
- CPU/disk/RDS alarms
- Synthetic API send probe
- Log archive to S3
Operations
- Backup of /opt/postal/config to S3
- Upgrade runbook documented
- DR drill scheduled
- On-call escalation defined
Networking (extended)
- Internet Gateway attached to VPC
- Public route table has 0.0.0.0/0 to IGW
- Private route tables for RDS subnets only
- NACLs documented (keep simple; SGs primary)
- Elastic IP association documented in runbook
- SMTP submission sources restricted in SG
- Outbound HTTPS allowed for Docker pulls and apt
- DNS resolver reachable from EC2
- NTP/chrony synchronized
- IPv6 posture documented (Postal docs show AAAA optional)
SMTP and API (extended)
- SMTP port 25 listener verified (
postal status) - SMTP submission on 587 tested if enabled
- HTTP API send test from application subnet
- Webhook endpoints reachable from worker
- Bounce webhook URL registered per mail server
- Message retention policy documented
- Maximum message size aligned with RDS innodb setting
- Rate limits per mail server reviewed
- Suppression lists exported periodically
- Dead-letter / held message review process defined
Compliance and audit (extended)
- CloudTrail enabled for AWS API changes
- Config rules for public SG detection
- GuardDuty enabled
- Inspector scans on EC2
- Log retention meets policy
- Access reviews scheduled quarterly
- Change management ticket for postal.yml edits
- Data classification for message metadata documented
- Encryption at rest on RDS and EBS verified
- Incident response playbook includes email outage
Scaling and HA (extended)
- Capacity model by mail server documented
- RDS storage autoscaling threshold set
- EC2 vertical scale trigger defined (CPU sustained > 70%)
- Second EC2 standby image documented for active-passive
- Config sync mechanism (S3 versioned bucket)
- Failover DNS/EIP runbook tested
- Performance baseline captured before scale-up
- Docker image version pinned in upgrade notes
- Worker restart procedure documented
- Load test on API path completed in staging
Webhooks, Bounces, and Event Handling
Postal exposes delivery events through webhooks configured per mail server. On AWS, terminate webhooks on HTTPS endpoints in your VPC (ALB + ACM is appropriate here — unlike SMTP).
| Event type | Operational use |
|---|---|
| MessageSent | Confirm injection accepted |
| MessageDelayed | Queue/backpressure signal |
| MessageDeliveryFailed | Hard/soft failure triage |
| MessageBounced | Suppression list updates |
| MessageHeld | Policy/quarantine review |
Point webhooks at an internal API Gateway or ALB-backed service with authentication. Log webhook payloads to S3 via Firehose for replay during incident analysis.
Upgrading Postal v3
Postal upgrades require reading official upgrading documentation before each change. Production-safe pattern on AWS:
- Snapshot RDS and
/opt/postal/configto S3. - Pull latest install helper:
cd /opt/postal/install && sudo git pull. - Run
postal upgrade(per current install CLI). - Verify
postal status— all containers healthy. - Send synthetic API + SMTP probes before restoring full traffic.
Never upgrade during peak send windows. Treat signing.key immutability as a hard constraint unless you understand DKIM and signing impacts.
CloudWatch Alarm Example
# Context: CloudFormation snippet — EC2 CPU high for Postal host
Resources:
PostalCpuAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: postal-ec2-cpu-high
Namespace: AWS/EC2
MetricName: CPUUtilization
Dimensions:
- Name: InstanceId
Value: i-0123456789abcdef0
Statistic: Average
Period: 300
EvaluationPeriods: 3
Threshold: 80
ComparisonOperator: GreaterThanThreshold
AlarmActions:
- arn:aws:sns:us-east-1:123456789012:email-ops-alertsAdd companion RDS DatabaseConnections and FreeStorageSpace alarms — worker backlog often correlates with DB pressure before CPU spikes on EC2.
IAM Instance Role (Minimal)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"cloudwatch:PutMetricData",
"logs:CreateLogStream",
"logs:PutLogEvents",
"s3:PutObject"
],
"Resource": "*"
}
]
}Tighten Resource ARNs to your Secrets Manager secret, log groups, and backup bucket in production accounts.
Postal v3 vs KumoMTA on AWS — Decision Detail
Both run on EC2 with Elastic IPs and Route53 DNS. The operational split:
| Dimension | Postal v3 | KumoMTA |
|---|---|---|
| Primary interface | Web UI + HTTP API | Policy files (Lua) + SMTP |
| Install model | Docker Compose via postal CLI | Native packages + systemd |
| Database | MariaDB required | Not required for core MTA |
| Best volume band | 50k-1M emails/day typical | 1M+ with shaping investment |
| Multi-tenant | Virtual mail servers (built-in) | Tenant metadata in policy |
| Learning curve | Lower for app developers | Higher; rewards dedicated mail ops |
Our take: start with Postal when the team needs a visible control plane and HTTP API sends. Graduate to KumoMTA when queue policy complexity and throughput outgrow UI-driven configuration — or move outbound to SES when ops cost exceeds ESP economics.
DNS Flow Diagram
flowchart TD
cfg[postal.yml dns block] --> r53[Route53]
r53 --> mainHost[postal.example.com A]
r53 --> spfInc[spf.postal.example.com TXT]
r53 --> rpDom[rp.postal.example.com MX TXT DKIM]
r53 --> routesDom[routes.postal.example.com MX]
mainHost --> ptr[PTR on Elastic IP]
ptr --> trust[MailboxProviderTrust]Disaster Recovery Flow
flowchart LR
outage[EC2Failure] --> promote[AttachEIPtoStandby]
promote --> restore[RestoreConfigFromS3]
restore --> rds[RDSMultiAZFailoverIfNeeded]
rds --> verify[APIandSMTPProbes]
verify --> traffic[RestoreSends]What to Do This Week
- Provision EC2 + RDS MariaDB + Elastic IP in a non-production AWS account.
- Run
postal bootstrapand pointpostal.ymlat RDS. - Publish Postal DNS records (spf, return-path, main host) in Route53.
- Create first virtual mail server; send API and SMTP test messages.
- Enable CloudWatch alarms and config backup to S3.
- Document IP warmup plan before customer traffic.
What This Post Doesn’t Cover
- Hosted Postal offerings (third-party managed Postal platforms).
- Full Kubernetes deployment of Postal (EC2 Docker host is the official path).
- Postal v2 migration specifics — see Postal upgrading docs.
- Legal/compliance counsel per jurisdiction.
For hybrid or migration paths, see Amazon SES consulting and How to Migrate from SendGrid to Amazon SES.
FactualMinds Expert Recommendations
FactualMinds is an AWS Select Tier Consulting Partner with email infrastructure expertise.
| Scenario | Recommendation |
|---|---|
| Self-hosted UI + API platform | Postal v3 on EC2 + RDS |
| High-volume policy MTA | KumoMTA on AWS |
| Managed deliverability | Amazon SES |
| ESP migration | SES migration service |
AWS Cloud Architect & AI Expert
AWS-certified cloud architect and AI expert with deep expertise in cloud migrations, cost optimization, and generative AI on AWS.




