How to Install Postal on Ubuntu 24.04 VPS: Self-Hosted Mailgun Alternative
Mailgun, SendGrid, and Postmark charge between $0.80 and $10 per thousand emails. When your application sends password resets, receipts, order confirmations, and transactional notifications at scale, those per-message fees add up fast — and you hand over every recipient address and email body to a third party in the process. This guide walks you through installing Postal, the open-source mail delivery platform used by companies like Krystal Hosting to send hundreds of millions of messages a month, on an Ubuntu 24.04 VPS using Docker Compose.
Want transactional email without the ops overhead? Our CloudCore Professional plan gives you the RAM, disk, and clean-IP network you need for reliable delivery. Launch a Postal-ready VPS in under 60 seconds.
Table of Contents
What is Postal?
Postal is an open-source mail delivery platform built specifically for sending transactional and marketing email from your own infrastructure. It is the self-hosted equivalent of Mailgun, SendGrid, Postmark, or Amazon SES — a full outbound (and optional inbound) SMTP server with a web dashboard, REST API, message queue, bounce handling, open and click tracking, suppression lists, webhooks, and per-domain DKIM signing. Postal was originally built by Krystal Hosting to power their own notification infrastructure, then open-sourced in 2017 under the MIT license.
Under the hood, Postal is a Ruby on Rails application backed by MariaDB (for user accounts, domains, and metadata), a message database (a dedicated MariaDB instance for storing message bodies and delivery logs), and RabbitMQ (for the delivery queue). Inbound SMTP is handled by a Go-based SMTP server, outbound delivery runs as background workers, and a web UI built on Rails gives you dashboards for every message your app sends. The Docker Compose installation bundles all of these services plus Caddy as an automatic HTTPS reverse proxy.
Postal is explicitly designed for application email — the messages your software generates, not personal inbox mail. Think password resets, account verification links, two-factor codes, receipts, order confirmations, shipping notifications, weekly digest emails, and marketing newsletters sent from your app. It is not a replacement for a mailbox server like Mailcow, Mail-in-a-Box, or iRedMail — Postal does not give you IMAP, webmail, calendars, or address books. Pair it with a mailbox server if you need both outbound application mail and personal mailboxes on the same domain.
Typical production use cases include SaaS applications sending signup and billing emails to thousands of users, e-commerce stores delivering order and shipping notifications, marketing teams sending segmented campaigns to opt-in lists, and internal platforms sending alerts, reports, and scheduled digests. Any time your code calls send_email() — Postal replaces the third-party API you would otherwise pay per message.
Why Self-Host Transactional Email?
Running Postal on your own VPS gives you concrete advantages over per-email SaaS providers:
- No per-email fees — One flat VPS cost regardless of volume. Send 10,000 or 10 million messages a month for the same monthly bill.
- Full data ownership — Recipient addresses, message bodies, click events, and suppression lists stay on your server. No third party reads your customers' mail.
- Unlimited domains and mail servers — Mailgun charges per domain after the first. Postal lets you run hundreds of domains on one install at no extra cost.
- GDPR and HIPAA friendly — Keep all PII on infrastructure you control, in a jurisdiction you choose. Sign your own BAA with your VPS provider if needed.
- Custom IP reputation — With dedicated IPs you control the warmup, complaint handling, and reputation. No shared-IP noisy-neighbour problems.
- No API rate limits — Cloud providers throttle bursts. Your Postal instance processes mail as fast as your VPS and RabbitMQ queue can handle it.
- Open-source and forkable — Inspect the code, add custom headers, integrate with your own suppression system. MIT licensed with an active community at github.com/postalserver/postal.
Cost Comparison: Postal vs. Mailgun vs. SendGrid
| Monthly Volume | Mailgun Foundation | SendGrid Essentials | Amazon SES | Postal on VPS |
|---|---|---|---|---|
| 50,000 emails | $35/mo | $19.95/mo | $5/mo (+ infra) | EUR 19.99/mo |
| 100,000 emails | $35/mo | $34.95/mo | $10/mo (+ infra) | EUR 19.99/mo |
| 500,000 emails | ~$90/mo | ~$90/mo | $50/mo (+ infra) | EUR 19.99/mo |
| 1,000,000 emails | ~$180/mo | ~$250/mo | $100/mo (+ infra) | EUR 19.99/mo (may need upgrade) |
| 5,000,000 emails | ~$750/mo | ~$900/mo | $500/mo (+ infra) | EUR 39.99/mo |
| Multiple domains | +$5-10/domain | Extra plan | Free | Free (unlimited) |
| Dedicated IP | +$59/mo | +$89.95/mo | $25/mo | Included |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- At least 2 GB of RAM (4 GB recommended for comfortable MariaDB + RabbitMQ + Rails headroom)
- At least 40 GB of disk (message database and logs grow quickly)
- A domain name you control DNS for (for example
example.com) — you will use a subdomain likepostal.example.comfor the web UI - Port 25 open outbound — critical. Many budget hosts block outbound port 25 to prevent spam. Confirm with your provider before you start. DigitalOcean and Vultr block it by default; Hetzner and vps-server.host allow it on request or by default.
- A clean IP address — check your assigned IP at mxtoolbox.com/blacklists.aspx before you send anything. Ask for a fresh IP if it is listed on Spamhaus, Barracuda, or UCEPROTECT.
- DNS access to add MX, A, SPF, DKIM, and CNAME records
Recommended Plan: CloudCore Professional>
For a production Postal install sending 100K-1M emails per month, we recommend the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Port 25 unblocked by default
- Unmetered bandwidth
- EUR 19.99/month>
This gives MariaDB, the message DB, RabbitMQ, and the Rails web workers plenty of headroom, with disk space for several months of message logs. For higher volumes or dedicated-IP setups, ask our team about clean-IP allocation.
Connect to your server via SSH to get started:
ssh root@your-server-ipStep 1: Prepare DNS Records
Before touching the server, create these DNS records at your registrar or DNS provider. Replace example.com with your real domain throughout this guide.
| Type | Name | Value | Purpose |
|---|---|---|---|
| A | postal.example.com | <your-server-ip> | Web UI and SMTP hostname |
| PTR (rDNS) | <your-server-ip> | postal.example.com | Reverse DNS — set at your VPS provider, not your DNS host |
Domain-level records (SPF, DKIM, Return-Path, DMARC) come later in Step 11 after Postal generates the DKIM key for your sending domain.
Step 2: Update the System and Open Ports
Start by updating packages and installing prerequisites.
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl ca-certificates gnupg ufwIf a kernel update was applied, reboot before continuing:
sudo rebootOpen the ports Postal needs. Postal uses port 25 for outbound SMTP delivery to other mail servers, port 2525 or 587 for your application to submit mail, and ports 80/443 for the web UI served by Caddy.
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP (Let's Encrypt challenge)
sudo ufw allow 443/tcp # HTTPS (web UI)
sudo ufw allow 25/tcp # SMTP (outbound delivery + inbound bounces)
sudo ufw allow 587/tcp # Submission (apps connect here with auth)
sudo ufw --force enable
sudo ufw statusConfirm your provider is not blocking outbound port 25:
nc -vz gmail-smtp-in.l.google.com 25Expected output:
Connection to gmail-smtp-in.l.google.com 25 port [tcp/smtp] succeeded!If the connection hangs or is refused, open a support ticket with your VPS provider to unblock port 25 before continuing — Postal is useless without it.
Step 3: Install Docker and Docker Compose
Postal is distributed as a set of Docker images, so you need Docker Engine and the Compose plugin. If you are not already comfortable with Docker, our companion guides walk through the full install and verify:
For the short version, install Docker from the official repository:curl -fsSL https://get.docker.com | sh
sudo systemctl enable --now dockerVerify both tools:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7Add your user to the docker group so you do not need sudo for every command (log out and back in afterwards):
sudo usermod -aG docker $USERStep 4: Clone the Postal Install Repository
Postal maintains a curated Docker Compose setup in a separate postal/install repository. Clone it to /opt/postal/install.
sudo mkdir -p /opt/postal
sudo git clone https://github.com/postalserver/install /opt/postal/installAdd the bundled postal CLI wrapper to your PATH so you can call commands like postal bootstrap and postal start directly.
sudo ln -s /opt/postal/install/bin/postal /usr/local/bin/postalVerify the CLI is on your PATH:
postal --helpExpected output (abbreviated):
Usage: postal [command]
Commands: bootstrap <hostname> Bootstrap a new Postal server initialize Initialize the database make-user Create a new admin user start Start all services stop Stop all services upgrade-db Run database migrations ...
Step 5: Bootstrap Postal
Run the bootstrap command with the hostname you created an A record for in Step 1. This generates postal.yml, a signing key, and a Caddyfile under /opt/postal/config.
sudo postal bootstrap postal.example.comExpected output:
/opt/postal/config is ready with:
- postal.yml
- signing.key
- Caddyfile
Please edit /opt/postal/config/postal.yml to set up the server.Peek at what was created:
ls /opt/postal/config/Caddyfile postal.yml signing.keypostal.yml— The main configuration file with database credentials, SMTP server settings, web UI host, and DNS defaults. You edit this in the next step.signing.key— A 2048-bit RSA private key Postal uses to sign internal tokens and DKIM by default. Never commit this to git.Caddyfile— A Caddy reverse-proxy config pre-populated with your hostname. Caddy handles Let's Encrypt certificate issuance automatically when you start the stack.
Step 6: Edit postal.yml
Open postal.yml in your editor:
sudo nano /opt/postal/config/postal.ymlThe bootstrap generates sensible defaults, but you should review and harden a few sections. Below is the minimum you need to verify or change. The values in this example work for a single-server install where MariaDB, RabbitMQ, and Postal all live in the same Docker network.
version: 2postal: web_hostname: postal.example.com smtp_hostname: postal.example.com
web_server: bind_address: 0.0.0.0 port: 5000
main_db: host: postal-mariadb port: 3306 username: postal password: CHANGE_ME_STRONG_PASSWORD database: postal
message_db: host: postal-mariadb port: 3306 username: postal password: CHANGE_ME_STRONG_PASSWORD prefix: postal
rabbitmq: host: postal-rabbitmq username: postal password: CHANGE_ME_RABBIT_PASSWORD vhost: /postal
dns: mx_records: - mx.postal.example.com smtp_server_hostname: postal.example.com spf_include: spf.postal.example.com return_path_domain: rp.postal.example.com route_domain: routes.postal.example.com track_domain: click.postal.example.com
smtp_server: port: 25 tls_enabled: true tls_certificate_path: /config/smtp.cert tls_private_key_path: /config/smtp.key
smtp: host: 127.0.0.1 port: 2525 username: ~ password: ~ from_name: Postal from_address: [email protected]
Key fields to set:
main_db.passwordandmessage_db.password— use the same strong password for both; they point at the same MariaDB container with different database prefixes. Generate withopenssl rand -base64 24.rabbitmq.password— another long random string.dns.mx_records— the hostname(s) you will publish as MX for bounce and inbound handling (typicallymx.postal.example.com).dns.spf_include,return_path_domain,route_domain,track_domain— subdomains Postal tells your users to add when they onboard a sending domain. You will create CNAMEs for these in Step 11.
Ctrl+O, Enter, Ctrl+X in nano).Step 7: Initialize the Database
Now start MariaDB and RabbitMQ and run Postal's database migrations.
sudo postal initializeExpected output (abbreviated):
[+] Running 2/2
✔ Container postal-mariadb Started
✔ Container postal-rabbitmq Started
Waiting for MariaDB to be ready...
Creating database 'postal'...
Running migrations...
-> migrating CreateOrganizations
-> migrating CreateUsers
-> migrating CreateServers
...
Database initialized successfully.If you see Access denied for user 'postal'@..., the MariaDB password in postal.yml does not match what was set when the container started. Stop everything, delete the MariaDB volume, fix the password, and re-run:
sudo postal stop
sudo docker volume rm postal_mariadb_data
sudo postal initializeStep 8: Create an Admin User
Postal ships with no default login. Create your first admin interactively:
sudo postal make-userYou will be prompted for:
Email address: [email protected]
First name: Yossef
Last name: Admin
Initial password [leave blank for random]: **
User created with admin privileges.Write the password down in your password manager now — Postal only displays it once. Repeat the command to add more admin users later.
Step 9: Start Postal and Configure Caddy
Start the full Postal stack, including the Caddy reverse proxy that handles HTTPS for the web UI:
sudo postal startExpected output:
[+] Running 7/7
✔ Container postal-mariadb Running
✔ Container postal-rabbitmq Running
✔ Container postal-web Started
✔ Container postal-worker Started
✔ Container postal-smtp Started
✔ Container postal-cron Started
✔ Container postal-caddy StartedVerify every container is healthy:
sudo docker ps --filter "name=postal-"All containers should show healthy or Up with no restart loops.
The Caddyfile Postal generated looks like this:
postal.example.com {
reverse_proxy postal-web:5000
}Caddy automatically requests a Let's Encrypt certificate the first time postal.example.com resolves to your server. Watch the logs while it issues:
sudo docker logs -f postal-caddyExpected output:
certificate obtained successfully identifier=postal.example.com
serving initial configurationFor a full Caddy walkthrough including custom certificates, rate-limiting, and security headers, see our How to Install Caddy on Ubuntu 24.04 and How to Set Up Let's Encrypt with Caddy guides.
Open https://postal.example.com in your browser. You should see the Postal login screen. Sign in with the admin credentials from Step 8.
Step 10: Create Organization, Mail Server, and Domain
Postal has a three-level hierarchy: Organization (typically your company), Mail Server (a group of sending domains and queues), and Domain (each sending domain, like mail.example.com).
Create an Organization
Acme Inc) and give it a short permalink (e.g. acme).Create a Mail Server
Production) and give it a permalink (e.g. production).Add a Sending Domain
mail.acme.com or acme.com).Step 11: Add SPF, DKIM, and Return-Path DNS
After adding a domain, Postal displays required DNS records. They look something like this (replace with the exact values Postal shows you):
| Type | Name | Value | Required |
|---|---|---|---|
| TXT | acme.com | v=spf1 a mx include:spf.postal.example.com ~all | Yes — SPF |
| CNAME | rp.acme.com | rp.postal.example.com | Yes — Return-Path |
| TXT | postal-abc123._domainkey.acme.com | v=DKIM1; t=s; h=sha256; p=MIIBIjAN... | Yes — DKIM |
| MX | acme.com (priority 10) | mx.postal.example.com | Optional — inbound routing |
- SPF — If you already have an SPF record for the domain, merge the
include:spf.postal.example.comdirective into it rather than creating a second record. SPF allows only one record per domain and multiple records cause validation failures. - Return-Path — The
rp.CNAME lets Postal receive bounces and complaint reports at a subdomain of your sending domain, which aligns DMARC and looks more trustworthy than bouncing through the Postal server's hostname. - DKIM — Postal generates a fresh 2048-bit DKIM key per domain when you add it. The selector (
postal-abc123) is unique per domain. Paste the value exactly, including thev=DKIM1; ...prefix. - MX — Only needed if you want Postal to receive mail for this domain (bounces, replies, inbound routing). For pure outbound setups you can skip it, but most users want it so Postal can process bounces automatically.
Add DMARC (Recommended)
Create one more TXT record to publish a DMARC policy:
Type: TXT
Name: _dmarc.acme.com
Value: v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100Start with p=quarantine while you verify alignment, then tighten to p=reject after a week of clean DMARC reports.
Step 12: Send Your First Email
Postal supports two ways to send: SMTP (compatible with every language and library) and HTTP API (faster, with structured responses). Start with SMTP.
Create SMTP Credentials
app-production) and choose type SMTP.your-server-token) and a password. Copy both.Send a Test with swaks
Install swaks and send a test message:
sudo apt install -y swaks
swaks --to [email protected] \
--from [email protected] \
--server postal.example.com \
--port 587 \
--auth LOGIN \
--auth-user YOUR_SMTP_USERNAME \
--auth-password YOUR_SMTP_PASSWORD \
--tls \
--body "Hello from self-hosted Postal."Expected output:
<- 250 2.0.0 Ok: queued as abc123
=== Connection closed with remote host.Within seconds the message should arrive in your inbox. Check the Postal dashboard — the message appears in Messages with full delivery status, headers, and DKIM signature.
Send via the HTTP API
Create an API credential the same way (pick type API instead of SMTP) and copy the API key. Then:
curl -X POST https://postal.example.com/api/v1/send/message \
-H "Content-Type: application/json" \
-H "X-Server-API-Key: YOUR_API_KEY" \
-d '{
"to": ["[email protected]"],
"from": "[email protected]",
"subject": "Hello from Postal",
"html_body": "<p>This message was sent via the Postal API.</p>",
"plain_body": "This message was sent via the Postal API."
}'Expected response:
{
"status": "success",
"time": 0.12,
"flags": {},
"data": {
"message_id": "[email protected]",
"messages": {
"[email protected]": { "id": 42, "token": "xyz789" }
}
}
}Step 13: Webhooks for Bounces and Click Tracking
Postal can POST JSON events to your application when messages bounce, are opened, clicked, or marked as spam. This is how you keep your user database in sync with delivery reality.
https://app.acme.com/webhooks/postal).MessageSent — delivered successfully
- MessageDelayed — temporary failure, will retry
- MessageDeliveryFailed — hard bounce
- MessageBounced — received a bounce back from the recipient MTA
- MessageLinkClicked — click tracking fired
- MessageLoaded — open tracking fired (tracking pixel loaded)
Example payload for a hard bounce:
{
"event": "MessageBounced",
"timestamp": 1713273600,
"payload": {
"original_message": { "id": 42, "token": "xyz789" },
"bounce": {
"id": 99,
"details": "550 5.1.1 No such user",
"code": "550",
"category": "InvalidRecipient"
}
}
}Use the bounce category to decide whether to mark the user as invalid (InvalidRecipient) vs temporarily retry (MailboxFull, DeferredMessage).
IP Pool Setup and Warmup
If you send enough volume to justify dedicated sending IPs, Postal lets you assign multiple outbound IPs and split traffic across them.
Add Additional IPs to Your VPS
On vps-server.host you can request additional IPv4 addresses. Assign them in your netplan config (/etc/netplan/01-netcfg.yaml), apply, and verify:
ip addr showCreate an IP Pool in Postal
warming-1) and add the IP you want to use.Warmup Schedule
New IPs have no reputation. Sending 100,000 messages on day one will torpedo you into spam folders across Gmail and Microsoft. Follow a gradual warmup:
| Day | Max per day (Gmail) | Max per day (Microsoft) | Notes |
|---|---|---|---|
| 1 | 50 | 50 | Only send to engaged, recent users |
| 2 | 100 | 100 | Watch bounce rate — abort if > 2% |
| 3 | 500 | 300 | |
| 4 | 1,000 | 500 | |
| 7 | 5,000 | 2,000 | Add a second IP if needed |
| 14 | 20,000 | 10,000 | |
| 30 | 100,000+ | 50,000+ | Full production volume |
- Send only to engaged recipients first — people who opened an email in the last 30 days.
- Keep complaint rate below 0.1% — enroll in Google Postmaster Tools and Microsoft SNDS to monitor it daily.
- Use consistent volume — a sudden spike after a quiet week looks like a compromised account.
- Set up feedback loops (FBLs) with each major ISP so you get complaint notifications.
Application Integration Examples
Postal speaks standard SMTP, so any mail library in any language works. Below are the most common app stacks.
Laravel (PHP)
Edit .env:
MAIL_MAILER=smtp
MAIL_HOST=postal.example.com
MAIL_PORT=587
MAIL_USERNAME=your-smtp-username
MAIL_PASSWORD=your-smtp-password
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="Acme"Clear config cache and send a test:
php artisan config:clear
php artisan tinker
> Mail::raw('Hello from Laravel via Postal', fn($m) => $m->to('[email protected]')->subject('Test'));Ruby on Rails
config/environments/production.rb:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'postal.example.com',
port: 587,
domain: 'acme.com',
user_name: ENV['POSTAL_SMTP_USER'],
password: ENV['POSTAL_SMTP_PASS'],
authentication: 'login',
enable_starttls_auto: true
}Node.js (Nodemailer)
import nodemailer from 'nodemailer';const transport = nodemailer.createTransport({ host: 'postal.example.com', port: 587, secure: false, // STARTTLS upgrades to TLS requireTLS: true, auth: { user: process.env.POSTAL_SMTP_USER, pass: process.env.POSTAL_SMTP_PASS, }, });
await transport.sendMail({ from: '"Acme" <[email protected]>', to: '[email protected]', subject: 'Hello from Node via Postal', text: 'Plain text version', html: '<p>HTML version</p>', });
Python (Django)
settings.py:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'postal.example.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = os.environ['POSTAL_SMTP_USER']
EMAIL_HOST_PASSWORD = os.environ['POSTAL_SMTP_PASS']
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'Acme <[email protected]>'Direct API (any language)
For higher throughput, hit the Postal HTTP API directly rather than SMTP. The API accepts JSON and returns immediately after queueing (no SMTP handshake overhead), which can be 2-5x faster than SMTP when sending large batches. See the full API reference at docs.postalserver.io/developer/api.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Connection timed out on port 25 | VPS provider blocks outbound 25 | Test with nc -vz gmail-smtp-in.l.google.com 25. Contact support to unblock, or switch to a provider that allows it (vps-server.host, Hetzner). |
| Mail lands in Gmail Spam | Missing or failing SPF/DKIM/DMARC, or bad IP reputation | Check record status in Postal UI. Test with dig TXT acme.com and mail-tester.com. Check IP at mxtoolbox.com/blacklists.aspx. |
| DKIM fails validation | TXT record wrapped or missing p= prefix | Paste the record value exactly as Postal displays, without line breaks. Some DNS UIs wrap long values — use their "raw" input if available. |
550 5.7.1 IP on Spamhaus ZEN bounce | Your server IP is listed on a major blocklist | Request delisting at spamhaus.org/lookup after fixing the cause. For SORBS/UCEPROTECT, delisting is sometimes automatic after 7 days of silence. |
Postal web UI shows 502 Bad Gateway | postal-web container not running or Caddy cannot reach it | sudo docker logs postal-web and sudo docker logs postal-caddy. Restart with sudo postal restart. |
ActiveRecord::RecordNotUnique on migrations | Database partially initialized from a previous attempt | sudo postal stop && sudo docker volume rm postal_mariadb_data && sudo postal initialize (destroys all data). |
Messages stuck in Held status | RabbitMQ not receiving jobs or workers not running | sudo docker logs postal-worker. Check RabbitMQ UI at localhost:15672. Restart worker. |
| Open/click tracking not recording | Tracking domain CNAME missing or HTTPS cert not issued | Verify click.postal.example.com resolves to your server. Check Caddy issued a cert: sudo docker logs postal-caddy \</td><td>grep click. |
530 5.7.0 Authentication required when sending | App connecting to port 25 instead of 587 with auth | Apps must use port 587 (or 2525) with SMTP AUTH. Port 25 is for server-to-server delivery only. |
Viewing Logs
Tail the full stack:
sudo docker compose -f /opt/postal/install/docker-compose.yml logs -fOr just one service:
sudo docker logs -f postal-worker
sudo docker logs -f postal-smtp
sudo docker logs -f postal-webFAQ
Is Postal a replacement for Gmail or Mailcow?
No. Postal is built for outbound application email only — password resets, receipts, notifications, marketing. It does not give you IMAP, webmail, calendars, or human mailboxes. If you need [email protected] as a personal email address you can check from Gmail or Apple Mail, run a mailbox server like Mailcow, Mail-in-a-Box, or iRedMail alongside Postal. The two can coexist on different subdomains (mail.acme.com for mailboxes, postal.acme.com for application mail).
Do I really need port 25 open outbound?
Yes. Port 25 is how Postal talks to recipient mail servers like Gmail, Outlook, and Yahoo to actually deliver your messages. If your VPS provider blocks outbound 25, Postal can queue messages but never deliver them. DigitalOcean, Vultr, Google Cloud, and AWS EC2 all block it by default (though AWS will unblock on request). Hetzner, OVH, Contabo, and vps-server.host generally allow it. Always confirm with nc -vz gmail-smtp-in.l.google.com 25 before investing time in setup.
How do I keep my IP reputation clean?
Three things matter most: low bounce rate (under 2%), low complaint rate (under 0.1%), and consistent, gradual volume (no sudden spikes). Enroll in Google Postmaster Tools and Microsoft SNDS on day one — they give you daily visibility into spam rate, IP reputation, and authentication pass rates. Honor unsubscribes within 24 hours (Postal has a built-in suppression list — always check it before sending). Never buy email lists. Follow the warmup schedule in the IP Pool section above.
How does Postal compare to Mailgun, SendGrid, and Amazon SES?
Postal is self-hosted, open-source, unlimited volume at a flat VPS cost, and gives you full data ownership and unlimited domains. You handle IP warmup, deliverability monitoring, and server ops yourself. Best for: teams with existing DevOps capacity sending more than ~50K emails/month, or anyone with strict data residency requirements.
Mailgun and SendGrid are managed SaaS with easy onboarding, good deliverability out of the box on shared IPs, and per-email pricing. They handle IP warmup and feedback loops but charge extra for dedicated IPs, and pricing scales linearly with volume. Best for: teams that want zero ops work and predictable SaaS billing at lower volumes.
Amazon SES is cheap ($0.10 per 1,000) with excellent deliverability on AWS-managed IPs. But it requires IAM and sandbox-approval workflow, throttles new accounts heavily, and lacks a built-in message UI (you need to build your own). Best for: teams already on AWS that are comfortable with AWS operational patterns.
For most teams sending more than 100K emails/month from their own domain with strict data control, self-hosted Postal wins on cost and flexibility.
Can I receive inbound email with Postal?
Yes, partially. Postal can receive mail for domains whose MX points at it and either route the message to another address, forward it to an HTTP endpoint, or expose it via the API. This is great for processing bounce reports, inbound replies to support tickets, or parsing structured emails (like receipts from Stripe webhooks). But Postal does not give you a mailbox where you can read mail interactively — for that you need a dedicated mailbox server.
How do I back up Postal?
The two volumes you need to back up are MariaDB (postal_mariadb_data) and /opt/postal/config/. Back up config with any file-sync tool. For MariaDB, run a nightly dump:
docker exec postal-mariadb mariadb-dump --all-databases -uroot -p"$ROOT_PASSWORD" | gzip > /var/backups/postal-$(date +%F).sql.gzPair with a restic or BorgBackup job that pushes the file to S3, Backblaze B2, or another VPS. The RabbitMQ queue does not need backing up — it holds only in-flight messages, which Postal retries automatically after a restart.
Next Steps
Now that Postal is sending mail on your VPS, here are recommended next steps to build a production-grade delivery platform:
- Set up monitoring with Uptime Kuma — Deploy Uptime Kuma to monitor the Postal web UI, SMTP port 587, and port 25 outbound connectivity. Add alerts for downtime or certificate expiry.
- Install Grafana for deliverability dashboards — Pipe Postal's MySQL metrics into Grafana for long-term trend analysis on send volume, bounce rate by domain, and click-through rates.
- Harden with Fail2ban — Protect port 587 from brute-force SMTP AUTH attacks with Fail2ban. Add a filter that watches the Postal SMTP container logs for repeated auth failures.
- Add a WAF with CrowdSec — Install CrowdSec in front of the web UI to block credential stuffing against
/loginand share threat intel with the CrowdSec community.
- Integrate with your CRM or Mautic — Postal works as the SMTP backend for marketing automation tools like Mautic or customer.io. Configure them to send via Postal SMTP and you get unlimited campaign volume at flat cost.
- Read the official Postal docs — The docs.postalserver.io site covers advanced topics: multi-server clustering, custom bounce handlers, API pagination, and contributor guides.
Skip the Manual Install — Deploy Postal on a Clean-IP VPS>
Our CloudCore Professional plan gives you everything Postal needs out of the box: 6 vCPU, 12 GB RAM, 100 GB NVMe, port 25 unblocked, and a clean IP with reverse DNS you control. No support ticket required to start sending.>
- Ubuntu 24.04 LTS pre-installed
- Port 25 open by default
- Custom PTR/rDNS in your control panel
- Additional IPs available for IP pool warmup
- Docker and Docker Compose one-click ready>
Deploy Your Postal VPS Now — Plans start at EUR 19.99/month.