Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Postal Ubuntu
GUIDEInstall Guides

How to Install Postal on Ubuntu 24.04 VPS: Self-Hosted Mailgun Alternative

27 min read

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?
  • Why Self-Host Transactional Email?
  • Prerequisites
  • Step 1: Prepare DNS Records
  • Step 2: Update the System and Open Ports
  • Step 3: Install Docker and Docker Compose
  • Step 4: Clone the Postal Install Repository
  • Step 5: Bootstrap Postal
  • Step 6: Edit postal.yml
  • Step 7: Initialize the Database
  • Step 8: Create an Admin User
  • Step 9: Start Postal and Configure Caddy
  • Step 10: Create Organization, Mail Server, and Domain
  • Step 11: Add SPF, DKIM, and Return-Path DNS
  • Step 12: Send Your First Email
  • Step 13: Webhooks for Bounces and Click Tracking
  • IP Pool Setup and Warmup
  • Application Integration Examples
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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 VolumeMailgun FoundationSendGrid EssentialsAmazon SESPostal 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/domainExtra planFreeFree (unlimited)
    Dedicated IP+$59/mo+$89.95/mo$25/moIncluded
    For any application sending more than ~20,000 messages per month, Postal pays for itself within the first few weeks.

    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 like postal.example.com for 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:

    bash
    ssh root@your-server-ip

    Step 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.

    TypeNameValuePurpose
    Apostal.example.com<your-server-ip>Web UI and SMTP hostname
    PTR (rDNS)<your-server-ip>postal.example.comReverse DNS — set at your VPS provider, not your DNS host
    Setting reverse DNS (PTR) to match the hostname your server uses for EHLO is non-negotiable for deliverability. Gmail, Microsoft, and Yahoo all reject or junk mail from servers whose PTR does not resolve back to a matching A record. Configure PTR in your VPS control panel (on vps-server.host the rDNS field is in the server detail view).

    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.

    bash
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y git curl ca-certificates gnupg ufw

    If a kernel update was applied, reboot before continuing:

    bash
    sudo reboot

    Open 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.

    bash
    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 status

    Confirm your provider is not blocking outbound port 25:

    bash
    nc -vz gmail-smtp-in.l.google.com 25

    Expected output:

    text
    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:

    • How to Install Docker on Ubuntu 24.04
    • How to Install Docker Compose on Ubuntu 24.04
    For the short version, install Docker from the official repository:

    bash
    curl -fsSL https://get.docker.com | sh
    sudo systemctl enable --now docker

    Verify both tools:

    bash
    docker --version
    docker compose version

    Expected output:

    text
    Docker version 27.3.1, build ce12230
    Docker Compose version v2.29.7

    Add your user to the docker group so you do not need sudo for every command (log out and back in afterwards):

    bash
    sudo usermod -aG docker $USER

    Step 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.

    bash
    sudo mkdir -p /opt/postal
    sudo git clone https://github.com/postalserver/install /opt/postal/install

    Add the bundled postal CLI wrapper to your PATH so you can call commands like postal bootstrap and postal start directly.

    bash
    sudo ln -s /opt/postal/install/bin/postal /usr/local/bin/postal

    Verify the CLI is on your PATH:

    bash
    postal --help

    Expected output (abbreviated):

    text
    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.

    bash
    sudo postal bootstrap postal.example.com

    Expected output:

    text
    /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:

    bash
    ls /opt/postal/config/
    text
    Caddyfile  postal.yml  signing.key
    • postal.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:

    bash
    sudo nano /opt/postal/config/postal.yml

    The 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.

    yaml
    version: 2

    postal: 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.password and message_db.password — use the same strong password for both; they point at the same MariaDB container with different database prefixes. Generate with openssl 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 (typically mx.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.
    Save and exit (Ctrl+O, Enter, Ctrl+X in nano).

    Step 7: Initialize the Database

    Now start MariaDB and RabbitMQ and run Postal's database migrations.

    bash
    sudo postal initialize

    Expected output (abbreviated):

    text
    [+] 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:

    bash
    sudo postal stop
    sudo docker volume rm postal_mariadb_data
    sudo postal initialize

    Step 8: Create an Admin User

    Postal ships with no default login. Create your first admin interactively:

    bash
    sudo postal make-user

    You will be prompted for:

    text
    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:

    bash
    sudo postal start

    Expected output:

    text
    [+] 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         Started

    Verify every container is healthy:

    bash
    sudo docker ps --filter "name=postal-"

    All containers should show healthy or Up with no restart loops.

    The Caddyfile Postal generated looks like this:

    text
    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:

    bash
    sudo docker logs -f postal-caddy

    Expected output:

    text
    certificate obtained successfully  identifier=postal.example.com
    serving initial configuration

    For 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

  • After logging in, click New Organization on the dashboard.
  • Name it (e.g. Acme Inc) and give it a short permalink (e.g. acme).
  • Create a Mail Server

  • Inside your organization, click New Mail Server.
  • Name it (e.g. Production) and give it a permalink (e.g. production).
  • Choose the Mode:
  • - Live — sends mail normally. Use for production. - Development — accepts mail but does not deliver externally. Safe for testing and staging.

    Add a Sending Domain

  • Inside the mail server, go to Domains → Add Domain.
  • Enter the domain you want to send from (e.g. mail.acme.com or acme.com).
  • Postal displays a DNS verification screen with four records it needs you to publish. Do not close this page — you need it for Step 11.
  • 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):

    TypeNameValueRequired
    TXTacme.comv=spf1 a mx include:spf.postal.example.com ~allYes — SPF
    CNAMErp.acme.comrp.postal.example.comYes — Return-Path
    TXTpostal-abc123._domainkey.acme.comv=DKIM1; t=s; h=sha256; p=MIIBIjAN...Yes — DKIM
    MXacme.com (priority 10)mx.postal.example.comOptional — inbound routing
    Important notes on each record:

    • SPF — If you already have an SPF record for the domain, merge the include:spf.postal.example.com directive 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 the v=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.
    After adding the records, wait 60-120 seconds for DNS to propagate, then click Check DNS in the Postal UI. All four rows should turn green.

    Add DMARC (Recommended)

    Create one more TXT record to publish a DMARC policy:

    text
    Type:  TXT
    Name:  _dmarc.acme.com
    Value: v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100

    Start 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

  • Inside your mail server, go to Credentials → New Credential.
  • Name it (e.g. app-production) and choose type SMTP.
  • Postal displays a username (your-server-token) and a password. Copy both.
  • Send a Test with swaks

    Install swaks and send a test message:

    bash
    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:

    text
    <-  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:

    bash
    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:

    json
    {
      "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.

  • In the Postal UI, go to Mail Server → Webhooks → New Webhook.
  • Set the URL to your application endpoint (e.g. https://app.acme.com/webhooks/postal).
  • Select the events you want:
  • - 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)
  • Save. Postal signs every payload with HMAC-SHA256 using your webhook secret — verify the signature on your end before trusting the payload.
  • Example payload for a hard bounce:

    json
    {
      "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:

    bash
    ip addr show

    Create an IP Pool in Postal

  • Go to Admin → IP Pools → New IP Pool.
  • Name it (e.g. warming-1) and add the IP you want to use.
  • Assign the pool to a mail server in Mail Server → Settings → IP Pool.
  • 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:

    DayMax per day (Gmail)Max per day (Microsoft)Notes
    15050Only send to engaged, recent users
    2100100Watch bounce rate — abort if > 2%
    3500300
    41,000500
    75,0002,000Add a second IP if needed
    1420,00010,000
    30100,000+50,000+Full production volume
    Tips for a clean warmup:

    • 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:

    text
    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:

    bash
    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:

    ruby
    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)

    javascript
    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:

    python
    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

    ProblemCauseSolution
    Connection timed out on port 25VPS provider blocks outbound 25Test 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 SpamMissing or failing SPF/DKIM/DMARC, or bad IP reputationCheck record status in Postal UI. Test with dig TXT acme.com and mail-tester.com. Check IP at mxtoolbox.com/blacklists.aspx.
    DKIM fails validationTXT record wrapped or missing p= prefixPaste 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 bounceYour server IP is listed on a major blocklistRequest 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 Gatewaypostal-web container not running or Caddy cannot reach itsudo docker logs postal-web and sudo docker logs postal-caddy. Restart with sudo postal restart.
    ActiveRecord::RecordNotUnique on migrationsDatabase partially initialized from a previous attemptsudo postal stop && sudo docker volume rm postal_mariadb_data && sudo postal initialize (destroys all data).
    Messages stuck in Held statusRabbitMQ not receiving jobs or workers not runningsudo docker logs postal-worker. Check RabbitMQ UI at localhost:15672. Restart worker.
    Open/click tracking not recordingTracking domain CNAME missing or HTTPS cert not issuedVerify 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 sendingApp connecting to port 25 instead of 587 with authApps must use port 587 (or 2525) with SMTP AUTH. Port 25 is for server-to-server delivery only.

    Viewing Logs

    Tail the full stack:

    bash
    sudo docker compose -f /opt/postal/install/docker-compose.yml logs -f

    Or just one service:

    bash
    sudo docker logs -f postal-worker
    sudo docker logs -f postal-smtp
    sudo docker logs -f postal-web

    FAQ

    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:

    bash
    docker exec postal-mariadb mariadb-dump --all-databases -uroot -p"$ROOT_PASSWORD" | gzip > /var/backups/postal-$(date +%F).sql.gz

    Pair 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 /login and 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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket