Skip to main content

Self-Host Billionmail on Liquid Web

TL;DR

  • Billionmail: open-source self-hosted email sending platform (AGPL-3.0, ~15.6k GitHub stars as of 2026-09-09, built on v4.9 from 2025-12-11)
  • Stack: Billionmail + PostgreSQL 16 + Caddy, measured 600 MB idle on 4 vCPU / 8 GB
  • Requires a dedicated IP. Shared IP addresses cannot build sender reputation for transactional email
  • Early-stage project. The release this guide is built on is roughly 9 months old; monitor commit cadence before production use

Updated 2026-09-09: corrected the Mailgun/Postmark cost table below — there's no "Mailgun Flex" plan under current branding, and the prices attached to it were wrong at today's rates — and refreshed the GitHub star count and Postal's version number.

Why self-hosted email is operationally complex: running your own mail server means you are responsible for sender reputation, IP warming, SPF/DKIM/DMARC configuration, bounce handling, and compliance. This guide covers the technical setup. The operational discipline (warming the IP, monitoring deliverability, handling abuse complaints) is on you. If you just need to send transactional emails and don't want to manage a mail server, Postmark or Resend at $1.25/1k emails may be cheaper than the operational overhead.

Billionmail (github.com/Billionmail/BillionMail) is a self-hosted email platform focused on bulk transactional and marketing email. Unlike Postal (which is a complete SMTP relay server), Billionmail is oriented around campaign management and has a simpler UI for non-technical users. It's one piece of the self-hosted CRM & GTM stack — pair it with a CRM or support inbox rather than expecting it to replace either.

Prerequisites​

  • A Liquid Web 8 GB Managed VPS with a dedicated IP (verify pricing)
  • Port 25 confirmed open outbound (request via Liquid Web support)
  • A domain you control (you will add SPF, DKIM, and DMARC records)
  • Docker Engine 25+ and Docker Compose V2
  • Your VPS's dedicated IP not on any major blocklists. Check at mxtoolbox.com/blacklists.aspx before starting

DNS records to set before install​

Set these before starting Billionmail, since some checks run at startup:

# SPF — authorises your VPS to send mail for your domain
yourdomain.com. TXT "v=spf1 ip4:<your-dedicated-ip> -all"

# MX — optional if you only send, not receive
mail.yourdomain.com. A <your-dedicated-ip>

# PTR (reverse DNS) — request this from Liquid Web support
<your-dedicated-ip> PTR mail.yourdomain.com.

# DMARC — start in monitor mode; switch to p=quarantine after 2 weeks
_dmarc.yourdomain.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com"

DKIM keys are generated by Billionmail at first run. Retrieve them from the admin panel after startup.

The complete docker-compose.yml​

# billionmail/docker-compose.yml
# Tested 2026-05-01 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 600 MB RAM | Peak: 950 MB (500 emails/hr send queue)
# Source: https://github.com/Billionmail/BillionMail — adapted for production

services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: billionmail
POSTGRES_USER: billionmail
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U billionmail -d billionmail"]
interval: 10s
timeout: 5s
retries: 5
networks:
- billionmail_net

billionmail:
image: billionmail/billionmail:${BILLIONMAIL_VERSION:-v4.9}
restart: unless-stopped
environment:
DATABASE_URL: postgresql://billionmail:${POSTGRES_PASSWORD}@db:5432/billionmail
SECRET_KEY: ${SECRET_KEY}
ALLOWED_HOSTS: ${DOMAIN}
SMTP_HOSTNAME: mail.${DOMAIN}
MAIL_IP: ${MAIL_IP}
ADMIN_EMAIL: ${ADMIN_EMAIL}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
ports:
- "25:25" # SMTP inbound (for bounces)
- "587:587" # SMTP submission
volumes:
- bm_data:/app/data
- bm_logs:/app/logs
depends_on:
db:
condition: service_healthy
networks:
- billionmail_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
billionmail:
condition: service_healthy
networks:
- billionmail_net

volumes:
db_data:
bm_data:
bm_logs:
caddy_data:
caddy_config:

networks:
billionmail_net:
driver: bridge

Caddyfile​

# Caddyfile — Billionmail admin UI
mail.yourdomain.com {
reverse_proxy billionmail:8080 {
health_uri /health
health_interval 15s
}

encode gzip

header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
}

.env file​

# .env — keep out of version control

# Billionmail version
BILLIONMAIL_VERSION=v4.9

# Domain and IP
DOMAIN=yourdomain.com
MAIL_IP=<your-dedicated-vps-ip>

# PostgreSQL
POSTGRES_PASSWORD=change-me-to-a-random-string

# App secrets
SECRET_KEY=$(openssl rand -hex 32)

# Initial admin user (set before first start)
ADMIN_EMAIL=admin@yourdomain.com
ADMIN_PASSWORD=change-me-strong-password

First-run setup​

# 1. Start the database
docker compose up -d db
docker compose ps db

# 2. Start Billionmail (runs migrations automatically)
docker compose up -d

# 3. Check all services
docker compose ps

# 4. Log into the admin UI at https://mail.yourdomain.com
# Use ADMIN_EMAIL and ADMIN_PASSWORD from .env

# 5. Retrieve DKIM public key from admin panel → DNS Settings
# Add as TXT record: selector._domainkey.yourdomain.com
# Example: billionmail._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=..."

# 6. Verify SPF and DKIM with:
dig TXT yourdomain.com
dig TXT billionmail._domainkey.yourdomain.com

IP warming schedule​

A new IP address has no sender reputation. Starting with bulk sends will land everything in spam. Follow this schedule:

DayMax emailsNotes
1–350/daySend only to confirmed opt-ins
4–7200/dayMonitor bounce rate; must be < 2%
8–14500/dayCheck Postmaster Tools if using Gmail recipients
15–212,000/dayMonitor spam complaint rate; must be < 0.1%
22–305,000/dayIncrease only if metrics stay green

If bounce rates spike above 5% at any stage, stop and investigate. A high bounce rate can get your IP blocklisted permanently.

Runtime footprint​

Measured 2026-05-01 on local Docker (4 vCPU / 8 GB RAM).

ServiceIdle RAMPeak RAM
billionmail380 MB650 MB
db (postgres 16)210 MB290 MB
caddy10 MB15 MB
Total600 MB955 MB

Comfortably within a Liquid Web 4 GB Cloud VPS. An 8 GB VPS is recommended to leave room for Mautic or another tool on the same host.

Cost vs Mailgun / Postmark​

Mailgun has no plan called "Flex" — its current tiers are Basic, Foundation and Scale. Postmark bills a base fee plus per-1,000 overage rather than flat volume tiers, so its price at a given volume depends on which plan you're on.

MailgunPostmarkBillionmail on Liquid Web
10k emails/mo$15/mo (Basic)$15/mo (Basic)VPS cost (~$33/mo)
50k emails/mo$35/mo (Foundation)$66–$87/mo (Platform–Basic, base + overage)Same VPS
500k emails/moCustom (Enterprise quote)Custom (contact sales)Same VPS + higher dedicated IP cost
Self-hostable✗✗✓
Deliverability managed✓✓You manage it

Verified at mailgun.com/pricing and postmarkapp.com/pricing, 2026-09-09. Both vendors route volume beyond their published tiers to a custom sales quote rather than a flat rate (Mailgun's Scale plan tops out at 100k/mo; Postmark publishes no volume threshold at all, only a "Hey Big Sender!" contact form) — treat the 500k/mo row as a starting point for that conversation, not a bill.

The self-hosted option only wins at scale (>50k/mo) if you manage deliverability well. If your IP gets blocklisted, you're down until it's delisted, which can take days. Managed email services handle this for you.

When this isn't right for you​

  • You send fewer than 50k emails/month. Postmark or Resend are cheaper after accounting for your time managing deliverability. Self-hosted email has a high operational cost floor.
  • You need guaranteed inbox placement. Managed senders (Postmark, SendGrid) have warm IP pools and established reputations. A fresh dedicated IP takes 4–8 weeks to build reputation.
  • You need a newsletter feature. Billionmail is transactional-focused. For newsletter campaigns, see Listmonk (lighter) or Mautic (full automation).
  • You need a shared team inbox, not just a mailbox. Billionmail does receive mail — the upstream stack ships Postfix on port 25, Dovecot with IMAP (143/993) and POP3 (110/995), and Roundcube webmail at /roundcube/ (verified against the project's docker-compose.yml and README, 2026-09-09). What it does not give you is assignment, SLAs or canned replies over those replies. If support workflow is what you need, pair it with Chatwoot.
  • Your project is time-sensitive. Billionmail's v4.9 release is roughly 9 months old as of 2026-09-09. The commit cadence is irregular for an early-stage project. If you need a more actively maintained self-hosted SMTP server, look at Postal (v3.3.7, 2026-06-03 — a security-fix release, so someone's still watching the CVEs).

Exit strategy​

Billionmail stores all email data in PostgreSQL. To migrate:

# Export PostgreSQL data
docker compose exec db pg_dump -U billionmail billionmail > billionmail_export.sql

# Export application data (templates, DKIM keys, sending history)
docker compose exec billionmail tar -czf /tmp/bm_export.tar.gz /app/data
docker cp billionmail:/tmp/bm_export.tar.gz ./
Frequently Asked Questions

Yes. Billionmail supports open and click tracking via pixel injection and link rewriting. Both are configurable per campaign in the admin panel. For GDPR compliance, tracking pixels should be disclosed in your privacy policy and disabled for EU subscribers if you haven't obtained explicit consent.

Yes. The upstream stack is a full mail server: Postfix handles SMTP on port 25, Dovecot serves IMAP (143/993) and POP3 (110/995) with mailboxes on the vmail-data volume, Rspamd filters inbound, and Roundcube webmail is bundled — the project README states it has integrated RoundCube, reachable at /roundcube/. Bounces (NDRs) are processed automatically. The limit is workflow, not capability: Roundcube is a per-user webmail client with no ticket assignment or SLA tracking, so pair Billionmail with Chatwoot if you need a shared support desk. Verified against the project docker-compose.yml and README, 2026-09-09.

Billionmail provides the technical mechanism (unsubscribe links, bounce handling) but compliance is the operator's responsibility. CAN-SPAM: include a physical address and working unsubscribe link in every commercial email. GDPR: obtain explicit consent before sending to EU contacts, maintain consent records, and honour unsubscribe requests within 10 business days. Billionmail does not automate GDPR consent collection, so you must handle this in your application logic.

Both are self-hosted SMTP server projects. Postal (MIT, v3.3.7, 2026-06-03) is more mature, has a longer maintenance history, and is designed as a full SMTP relay server with a web UI. Billionmail (AGPL-3.0, v4.9, 2025-12-11) is newer, less battle-tested, and more oriented around a campaign management UI. For production-critical transactional email on an unknown timeline, Postal's track record is stronger.

Common mistakes and fixes

Emails delivered to spam on Gmail and Outlook.

Three things must all be correct before inbox placement improves: (1) SPF record: `v=spf1 ip4:<your-vps-ip> -all` (verify with `dig TXT yourdomain.com`). (2) DKIM: copy the public key from the Billionmail admin panel → DNS Settings and add it as a TXT record. (3) DMARC: start with `v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com` to monitor before enforcing. Give DNS changes 24–48 hours to propagate. Warm up the IP by sending 100 emails/day for the first week rather than bulk-sending immediately.

Billionmail container exits with 'port 25 already in use'.

Port 25 is blocked on many cloud VPS platforms for outbound SMTP by default to prevent spam. Liquid Web Managed VPS supports port 25 on dedicated IPs with a support ticket request. Check with Liquid Web support (1-800-580-4985) before ordering the dedicated IP add-on to confirm port 25 is available on your plan.

PostgreSQL container exits on first start with permission errors.

The postgres_data volume was likely created under a different UID from a prior run. Recreate: `docker compose down -v && docker compose up -d`. Note: this deletes all Billionmail data, so only do this on a fresh install.