Skip to main content

Self-Host Billionmail on Liquid Web

TL;DR

  • Billionmail: open-source self-hosted email sending platform (AGPL-3.0, ~14.6k GitHub stars, 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. Latest release is 4 months old as of 2026-05-01; monitor commit cadence before production use

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.

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 FlexPostmarkBillionmail on Liquid Web
10k emails/mo$35/mo$15/moVPS cost (~$33/mo)
50k emails/mo$75/mo$75/moSame VPS
500k emails/mo$400/mo$595/moSame VPS + higher dedicated IP cost
Self-hostableโœ—โœ—โœ“
Deliverability managedโœ“โœ“You manage it

Prices subject to change. Verify at Mailgun and Postmark official sites.

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).
  • Your project is time-sensitive. Billionmail v4.9 is 4+ months old as of 2026-05-01. 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.6, 2026-04-28, healthier maintenance cadence).

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.

Billionmail listens on port 25 for inbound email, primarily to receive bounce notifications (NDRs) and process them automatically. It is not a full inbound mailbox solution. If you need inbound email (customer replies, support tickets), pair Billionmail with Chatwoot (which has inbound email support) or a separate mail server.

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.6, 2026-04-28) 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.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50