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:
| Day | Max emails | Notes |
|---|---|---|
| 1โ3 | 50/day | Send only to confirmed opt-ins |
| 4โ7 | 200/day | Monitor bounce rate; must be < 2% |
| 8โ14 | 500/day | Check Postmaster Tools if using Gmail recipients |
| 15โ21 | 2,000/day | Monitor spam complaint rate; must be < 0.1% |
| 22โ30 | 5,000/day | Increase 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).
| Service | Idle RAM | Peak RAM |
|---|---|---|
| billionmail | 380 MB | 650 MB |
| db (postgres 16) | 210 MB | 290 MB |
| caddy | 10 MB | 15 MB |
| Total | 600 MB | 955 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 Flex | Postmark | Billionmail on Liquid Web | |
|---|---|---|---|
| 10k emails/mo | $35/mo | $15/mo | VPS cost (~$33/mo) |
| 50k emails/mo | $75/mo | $75/mo | Same VPS |
| 500k emails/mo | $400/mo | $595/mo | Same 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 ./
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.



