---
title: How to Set Up Postal v3 on AWS for Production: The Ultimate Enterprise Guide
description: 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.
url: https://www.factualminds.com/blog/postal-v3-production-setup-aws-enterprise-guide/
datePublished: 2026-07-09T00:00:00.000Z
dateModified: 2026-07-09T00:00:00.000Z
author: palaniappan-p
category: Email Deliverability
tags: postal, email-deliverability, aws-ec2, email-infrastructure, self-hosted-mta, engineering-guide
---

# How to Set Up Postal v3 on AWS for Production: The Ultimate Enterprise Guide

> 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](https://docs.postalserver.io/getting-started/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](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-resource-limits.html) 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](https://github.com/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](/services/aws-ses/) and [SES migration](/services/aws-ses-migration/). Comparing bare MTA options? Read our [KumoMTA on AWS guide](/blog/kumomta-production-setup-aws-enterprise-guide/).

Artifacts: [README](https://www.factualminds.com/examples/postal-aws-production/README.md), [postal.yml example](https://www.factualminds.com/examples/postal-aws-production/config/postal.yml.example), [Route53 DNS template](https://www.factualminds.com/examples/postal-aws-production/dns/route53-records.example.txt), [CloudWatch agent config](https://www.factualminds.com/examples/postal-aws-production/observability/cloudwatch-agent.json), [readiness checklist](https://www.factualminds.com/examples/postal-aws-production/production-readiness-checklist.md).

> **Benchmark pattern (not a cited client)** — B2B SaaS notification platform, **50k-200k emails/day**, `t3.xlarge` EC2 + RDS `db.t4g.medium` MariaDB, 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 mariadb` example from Postal docs with the default password and `innodb_log_file_size` left 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 MB** `innodb_log_file_size` for a **15 MB** max message policy.

> **Reproduce this** — Copy templates from [`examples/postal-aws-production/`](https://www.factualminds.com/examples/postal-aws-production/README.md). Run `postal bootstrap` on 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](/services/aws-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](/blog/kumomta-production-setup-aws-enterprise-guide/)** for policy-heavy high-volume MTA; use **SES** when managed deliverability wins |

## What Is Postal v3?

[Postal](https://docs.postalserver.io/getting-started) 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](https://docs.postalserver.io/getting-started/installation) |

### 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

```mermaid
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](/blog/kumomta-production-setup-aws-enterprise-guide/) |
| AWS-native managed deliverability           | [Amazon SES](/services/aws-ses/)                                |
| Quick ESP migration                         | [SES migration](/services/aws-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://github.com/postalserver/install/blob/main/templates/docker-compose.v3.yml)). HTTPS is handled by **Caddy** in the default install path.

```mermaid
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 --> sm
```

### Why 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

```mermaid
flowchart LR
  igw[InternetGateway] --> pub[PublicSubnet]
  pub --> ec2[PostalEC2]
  pub --> eip[EIP]
  priv[PrivateSubnet] --> rds[RDSMariaDB]
  ec2 --> rds
```

### VPC 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](https://github.com/postalserver/install)). Install Docker Engine and Docker Compose plugin per [Docker docs](https://docs.docker.com/engine/install/ubuntu/) 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](https://docs.postalserver.io/getting-started/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

```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl jq
```

### 2) Docker (official)

```bash
# 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 version
```

### 3) Clone Postal v3 install helper

```bash
sudo git clone https://github.com/postalserver/install /opt/postal/install
sudo ln -sf /opt/postal/install/bin/postal /usr/bin/postal
```

### 4) Bootstrap configuration

```bash
# Context: replace postal.example.com with your Route53 hostname
sudo postal bootstrap postal.example.com
```

This 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](https://www.factualminds.com/examples/postal-aws-production/config/postal.yml.example).

```yaml
main_db:
  host: postal-db.xxxx.us-east-1.rds.amazonaws.com
  username: postal_admin
  password: '<from-secrets-manager>'
  database: postal
```

### 6) Initialize database and admin user

```bash
sudo postal initialize
sudo postal make-user
```

### 7) Start Postal

```bash
sudo postal start
sudo postal status
```

### 8) Start Caddy for TLS

```bash
# 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 \
  caddy
```

### 9) Verify

- Browse `https://postal.example.com` and log in
- Create first virtual mail server in UI
- Send test via API (see below)

## Configure Postal for Production

### DNS block in postal.yml

Per [DNS configuration docs](https://docs.postalserver.io/getting-started/dns-configuration):

```yaml
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.com
```

### IP 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

```bash
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](https://www.factualminds.com/examples/postal-aws-production/dns/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 upgrade` workflow
- **GuardDuty + Inspector** on EC2

### SSH hardening

```conf
# Context: /etc/ssh/sshd_config on Postal EC2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
```

## Monitoring Architecture

```mermaid
flowchart LR
  ec2[PostalEC2] --> cwa[CloudWatchAgent]
  cwa --> cwm[Metrics]
  cwa --> cwl[Logs]
  cwm --> alarms[Alarms]
  alarms --> sns[SNS]
  rds[RDS] --> rdsi[RDSMetrics]
  rdsi --> alarms
```

### Priority 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](https://www.factualminds.com/examples/postal-aws-production/observability/cloudwatch-agent.json) on the EC2 host.

## Logging Pipeline

```mermaid
flowchart TD
  docker[DockerContainers] --> syslog[HostSyslog]
  syslog --> cwl[CloudWatchLogs]
  cwl --> s3[S3Archive]
  s3 --> athena[Athena]
```

- Postal `logging.stdout: true` in config for container log capture
- Retention: 14-30 days CloudWatch hot, 90-365 days S3 cold

### Logrotate on EC2

```conf
# 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

```mermaid
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.

```mermaid
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

```conf
# Context: /etc/sysctl.d/99-postal.conf
net.core.somaxconn = 4096
net.ipv4.ip_local_port_range = 10240 65535
fs.file-max = 2097152
```

## Cost 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                      |

```mermaid
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 event
```

## Maintenance 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](https://docs.postalserver.io/getting-started/upgrading) before each change. Production-safe pattern on AWS:

1. Snapshot RDS and `/opt/postal/config` to S3.
2. Pull latest install helper: `cd /opt/postal/install && sudo git pull`.
3. Run `postal upgrade` (per current install CLI).
4. Verify `postal status` — all containers healthy.
5. 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

```yaml
# 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-alerts
```

Add companion RDS `DatabaseConnections` and `FreeStorageSpace` alarms — worker backlog often correlates with DB pressure before CPU spikes on EC2.

## IAM Instance Role (Minimal)

```json
{
  "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](/blog/kumomta-production-setup-aws-enterprise-guide/) when queue policy complexity and throughput outgrow UI-driven configuration — or move outbound to [SES](/services/aws-ses/) when ops cost exceeds ESP economics.

## DNS Flow Diagram

```mermaid
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

```mermaid
flowchart LR
  outage[EC2Failure] --> promote[AttachEIPtoStandby]
  promote --> restore[RestoreConfigFromS3]
  restore --> rds[RDSMultiAZFailoverIfNeeded]
  rds --> verify[APIandSMTPProbes]
  verify --> traffic[RestoreSends]
```

## What to Do This Week

1. Provision EC2 + RDS MariaDB + Elastic IP in a non-production AWS account.
2. Run `postal bootstrap` and point `postal.yml` at RDS.
3. Publish Postal DNS records (spf, return-path, main host) in Route53.
4. Create first virtual mail server; send API and SMTP test messages.
5. Enable CloudWatch alarms and config backup to S3.
6. 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](https://docs.postalserver.io/getting-started/upgrading).
- Legal/compliance counsel per jurisdiction.

For hybrid or migration paths, see [Amazon SES consulting](/services/aws-ses/) and [How to Migrate from SendGrid to Amazon SES](/blog/how-to-migrate-from-sendgrid-to-amazon-ses/).

## FactualMinds Expert Recommendations

FactualMinds is an **AWS Select Tier Consulting Partner** with [email infrastructure](/services/aws-ses/) expertise.

| Scenario                      | Recommendation                                                         |
| ----------------------------- | ---------------------------------------------------------------------- |
| Self-hosted UI + API platform | Postal v3 on EC2 + RDS                                                 |
| High-volume policy MTA        | [KumoMTA on AWS](/blog/kumomta-production-setup-aws-enterprise-guide/) |
| Managed deliverability        | [Amazon SES](/services/aws-ses/)                                       |
| ESP migration                 | [SES migration service](/services/aws-ses-migration/)                  |

[Contact us to discuss your email infrastructure →](/contact-us/)

## FAQ

### When should you NOT self-host Postal v3 on AWS?
Skip self-hosted Postal when your team cannot own DNS, deliverability operations, and platform upgrades. If you need turnkey inbox placement with minimal ops, Amazon SES or a managed ESP is usually faster and cheaper at low volume.

### Can I use the Postal install-ubuntu.v3.sh script in production?
No for production as-is. The install repo warns that prerequisite scripts use insecure database passwords. Use them for labs only; production should use Amazon RDS MariaDB 10.6+ with Secrets Manager-backed credentials.

### What could go wrong if MariaDB innodb_log_file_size is too small?
Large messages fail to persist. Postal documentation requires innodb_log_file_size at least 10 times your maximum message size. A 15 MB message limit needs roughly 150 MB log file sizing on RDS parameter groups.

### Does AWS block outbound port 25 for Postal?
Yes by default on EC2. Request AWS support to remove the restriction if Postal must deliver directly to recipient MX servers on port 25. Submission on 587 may still work depending on your architecture.

### Is Postal v3 the same as running Postfix?
No. Postal is a full mail delivery platform with web UI, HTTP API, virtual mail servers, and Dockerized workers. Postfix is a traditional MTA requiring separate tooling for UI, API, and multi-tenant management.

### Can CloudFront or ALB terminate Postal SMTP traffic?
No. SMTP must bind on the EC2 host with network_mode host in Postal v3 containers. CloudFront, WAF, and ALB apply to HTTPS surfaces only (optionally in front of Caddy if you replace the default TLS path).

### Postal v3 vs KumoMTA — which should AWS teams choose?
Choose Postal when you want a web-managed platform with HTTP API and virtual mail servers for SMB to mid-market volume. Choose KumoMTA when you need high-volume policy-driven MTA automation with Lua shaping at scale.

### How do applications send mail through Postal?
Use the HTTP API with X-Server-API-Key per virtual mail server, or SMTP submission. API credentials are created in the Postal web UI after postal make-user and mail server setup.

---

*Source: https://www.factualminds.com/blog/postal-v3-production-setup-aws-enterprise-guide/*
