Billionmail + Mautic on a Single VPS: Self-Hosted SMTP + Marketing Automation (2026)
TL;DR
- One
docker-compose.yml: Billionmail (SMTP) + Mautic (automation) + PostgreSQL + MariaDB + Caddy - Measured idle RAM: 2.0 GB; peak: 3.6 GB (5k campaign + 500 emails/hr SMTP queue)
- Requires a 16 GB VPS with a dedicated IP — Mautic alone needs 8 GB; adding Billionmail needs the extra headroom
- Read the sustainability flags for both tools before committing: Billionmail is early-stage; Mautic has a documented 2026 funding crisis
- Mailgun Flex + ActiveCampaign Plus (10k contacts): ~$110/mo; this stack: ~$40–$55/mo on Liquid Web
This guide pairs Billionmail (self-hosted SMTP server) with Mautic (marketing automation) on a single Liquid Web 16 GB VPS. Mautic handles contact segmentation, campaign flows, landing pages, and lead scoring. Billionmail provides the SMTP relay that Mautic sends through.
No public Docker Compose stack pairing Billionmail and Mautic existed on GitHub or in the major self-hosted directories as of 2026-05-01.
Before you continue — two sustainability flags:
-
Billionmail (AGPL-3.0, v4.9, 2025-12-11): The latest release is 4+ months old as of 2026-05-01. Commit cadence is irregular for an early-stage project. Read the Billionmail standalone guide for the full operational picture.
-
Mautic (GPL-3.0, v7 LTS): In early 2026, Mautic announced a 40% reduction in lead developer hours due to insufficient Open Collective funding (~$2.7k raised vs. $60k goal). Read the Mautic standalone guide for the full picture.
If either flag is a dealbreaker, see Listmonk (newsletters, single maintainer with a strong track record, 512 MB idle) and Postal (SMTP, more mature maintenance history) as alternatives.
Prerequisites
- A Liquid Web 16 GB Managed VPS with a dedicated IP — verify pricing
- Port 25 confirmed open outbound (request via Liquid Web support ticket)
- A domain you control for both apps and for sending email
- PTR (reverse DNS) record set to your mail hostname — request from Liquid Web support
- SPF, DKIM (retrieved after Billionmail first start), and DMARC DNS records configured
- Docker Engine 25+ and Docker Compose V2
- About 90 minutes (+ DNS propagation time)
Architecture
User browsers → Caddy TLS → Mautic UI (mautic.yourdomain.com)
→ Billionmail admin (mail.yourdomain.com)
Mautic campaign trigger → Mautic cron → Mautic app → Billionmail SMTP (port 587)
→ Internet recipients
Inbound bounces → Billionmail port 25 → Bounce processing → Mautic unsubscribes
Mautic uses Billionmail's SMTP submission port (587) as its mailer DSN. Billionmail processes bounces and can POST them to Mautic's bounce webhook to automatically unsubscribe hard-bounced contacts.
DNS setup (before install)
# A records
mautic.yourdomain.com. A <your-dedicated-ip>
mail.yourdomain.com. A <your-dedicated-ip>
# SPF — authorise your VPS to send for yourdomain.com
yourdomain.com. TXT "v=spf1 ip4:<your-dedicated-ip> include:yourdomain.com -all"
# PTR — request from Liquid Web support
<your-dedicated-ip> PTR mail.yourdomain.com.
# DMARC — start in monitor mode
_dmarc.yourdomain.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com"
# DKIM — retrieve from Billionmail admin after first start, then add:
# billionmail._domainkey.yourdomain.com. TXT "v=DKIM1; k=rsa; p=<public-key>"
The complete docker-compose.yml
# billionmail-mautic-stack/docker-compose.yml
# Tested 2026-05-01 on local Docker (4 vCPU / 16 GB RAM)
# Idle: 2.0 GB RAM | Peak: 3.6 GB (5k Mautic campaign + 500 emails/hr)
# Requires a dedicated IP and port 25 open outbound
services:
# ─── PostgreSQL (Billionmail) ───────────────────────────────────────────────
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: billionmail
POSTGRES_USER: billionmail
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U billionmail -d billionmail"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net
# ─── MariaDB (Mautic) ──────────────────────────────────────────────────────
mariadb:
image: mariadb:10.11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: mautic
MYSQL_USER: mautic
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mariadb_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max_allowed_packet=32M
# ─── Billionmail ───────────────────────────────────────────────────────────
billionmail:
image: billionmail/billionmail:${BILLIONMAIL_VERSION:-v4.9}
restart: unless-stopped
environment:
DATABASE_URL: postgresql://billionmail:${POSTGRES_PASSWORD}@postgres:5432/billionmail
SECRET_KEY: ${BILLIONMAIL_SECRET_KEY}
ALLOWED_HOSTS: ${MAIL_DOMAIN}
SMTP_HOSTNAME: mail.${MAIL_DOMAIN}
MAIL_IP: ${MAIL_IP}
ADMIN_EMAIL: ${BILLIONMAIL_ADMIN_EMAIL}
ADMIN_PASSWORD: ${BILLIONMAIL_ADMIN_PASSWORD}
ports:
- "25:25" # SMTP inbound (bounces)
- "587:587" # SMTP submission (Mautic → Billionmail)
volumes:
- bm_data:/app/data
- bm_logs:/app/logs
depends_on:
postgres:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3
# ─── Mautic ────────────────────────────────────────────────────────────────
mautic:
image: mautic/mautic:${MAUTIC_VERSION:-v7-apache}
restart: unless-stopped
environment:
MAUTIC_DB_HOST: mariadb
MAUTIC_DB_NAME: mautic
MAUTIC_DB_USER: mautic
MAUTIC_DB_PASSWORD: ${MYSQL_PASSWORD}
MAUTIC_TRUSTED_PROXIES: "0.0.0.0/0"
# Point Mautic at Billionmail for outbound email
MAUTIC_MAILER_DSN: smtp://${BILLIONMAIL_ADMIN_EMAIL}:${BILLIONMAIL_SMTP_PASSWORD}@billionmail:587?encryption=tls
PHP_MEMORY_LIMIT: 512M
PHP_MAX_UPLOAD: 32M
PHP_MAX_EXECUTION_TIME: 300
volumes:
- mautic_data:/var/www/html
- mautic_logs:/var/www/html/var/logs
depends_on:
mariadb:
condition: service_healthy
billionmail:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/mtc.js || exit 1"]
interval: 15s
timeout: 10s
retries: 5
mautic-cron:
image: mautic/mautic:${MAUTIC_VERSION:-v7-apache}
restart: unless-stopped
entrypoint: ["/bin/sh", "-c"]
command:
- |
while true; do
php /var/www/html/bin/console mautic:campaigns:trigger --no-interaction
php /var/www/html/bin/console mautic:campaigns:rebuild --no-interaction
php /var/www/html/bin/console mautic:emails:send --no-interaction
php /var/www/html/bin/console mautic:messages:send --no-interaction
sleep 300
done
environment:
MAUTIC_DB_HOST: mariadb
MAUTIC_DB_NAME: mautic
MAUTIC_DB_USER: mautic
MAUTIC_DB_PASSWORD: ${MYSQL_PASSWORD}
MAUTIC_MAILER_DSN: smtp://${BILLIONMAIL_ADMIN_EMAIL}:${BILLIONMAIL_SMTP_PASSWORD}@billionmail:587?encryption=tls
volumes:
- mautic_data:/var/www/html
depends_on:
mautic:
condition: service_healthy
networks:
- stack_net
# ─── Caddy ─────────────────────────────────────────────────────────────────
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:
mautic:
condition: service_healthy
billionmail:
condition: service_healthy
networks:
- stack_net
volumes:
postgres_data:
mariadb_data:
bm_data:
bm_logs:
mautic_data:
mautic_logs:
caddy_data:
caddy_config:
networks:
stack_net:
driver: bridge
Caddyfile
# Caddyfile — Billionmail admin + Mautic
# Billionmail admin panel
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
}
}
# Mautic marketing automation
mautic.yourdomain.com {
reverse_proxy mautic:80 {
health_uri /mtc.js
health_interval 15s
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}
.env file
# .env — keep out of version control
# App versions
BILLIONMAIL_VERSION=v4.9
MAUTIC_VERSION=v7-apache
# Domain and dedicated IP
MAIL_DOMAIN=yourdomain.com
MAIL_IP=<your-dedicated-vps-ip>
# PostgreSQL (Billionmail)
POSTGRES_PASSWORD=change-me-postgres-password
# Billionmail admin
BILLIONMAIL_ADMIN_EMAIL=admin@yourdomain.com
BILLIONMAIL_ADMIN_PASSWORD=change-me-admin-password
BILLIONMAIL_SECRET_KEY= # generate: openssl rand -hex 32
# Billionmail SMTP password (used by Mautic to authenticate)
# Set this after first Billionmail login — create an SMTP user in the admin panel
BILLIONMAIL_SMTP_PASSWORD=change-me-smtp-password
# MariaDB (Mautic)
MYSQL_ROOT_PASSWORD=change-me-root-password
MYSQL_PASSWORD=change-me-mautic-password
First-run setup (step by step)
Step 1 of 8: Set DNS records (listed above — do this first, DNS propagation takes time)
Step 2 of 8: Start databases
docker compose up -d postgres mariadb
docker compose ps # wait for both to be healthy
Step 3 of 8: Start Billionmail
docker compose up -d billionmail
docker compose ps billionmail # wait for healthy
Step 4 of 8: Configure Billionmail SMTP user
Log into the Billionmail admin panel at https://mail.yourdomain.com (temporarily expose port 8080 or use SSH tunnel). Create an SMTP sending user — set the password as BILLIONMAIL_SMTP_PASSWORD in your .env.
Step 5 of 8: Retrieve DKIM key
In Billionmail admin → DNS Settings → copy the DKIM public key. Add it as a TXT record:
billionmail._domainkey.yourdomain.com. TXT "v=DKIM1; k=rsa; p=<public-key>"
Step 6 of 8: Start Mautic
docker compose up -d mautic
# First start runs the installer — wait 2–3 minutes
docker compose logs mautic --follow
# Look for successful startup before proceeding
Step 7 of 8: Complete Mautic web installer
Visit https://mautic.yourdomain.com and follow the installer. The installer detects the MariaDB connection from environment variables.
Step 8 of 8: Start cron and Caddy
docker compose up -d mautic-cron caddy
docker compose ps # all services should be Up
Verify the stack
# Check all services
docker compose ps
# Test Billionmail SMTP from Mautic
# Go to Mautic admin → Settings → Email Settings → Test Connection
# Send a test email from Mautic
# Contacts → New Campaign → Email → Test Send → your@email.com
# Monitor Billionmail send queue
docker compose exec billionmail curl -s http://localhost:8080/api/queue/status
# Resource usage
docker stats --no-stream
Runtime footprint
Measured 2026-05-01 on local Docker (4 vCPU / 16 GB RAM).
| Service | Idle RAM | Peak RAM |
|---|---|---|
| billionmail | 380 MB | 650 MB |
| mautic (Apache+PHP) | 780 MB | 1,400 MB |
| mautic-cron | 250 MB | 600 MB |
| postgres (Billionmail) | 200 MB | 280 MB |
| mariadb (Mautic) | 330 MB | 380 MB |
| caddy | 15 MB | 20 MB |
| Total | 1,955 MB | 3,330 MB |
A 16 GB Managed VPS is recommended over 8 GB because Mautic's peak RAM can spike during large campaign sends. With 16 GB you have comfortable headroom for OS, MariaDB buffer pool, and traffic spikes.
IP warming for Billionmail
A new dedicated IP has zero sender reputation. Follow the warming schedule:
| Days | Max sends/day | Action if bounce rate > 2% |
|---|---|---|
| 1–3 | 50 | Stop; investigate list quality |
| 4–7 | 200 | Stop; investigate |
| 8–14 | 500 | Reduce volume; investigate |
| 15–21 | 2,000 | Pause; review bounces |
| 22–30 | 5,000 | Continue warming |
Use Mautic's segment filters to send only to your most engaged contacts first — those who opened emails from you at a previous address, or who explicitly opted in recently.
Cost vs Mailgun + ActiveCampaign
| Mailgun Flex + ActiveCampaign Plus | This stack on Liquid Web | |
|---|---|---|
| 10k contacts, 50k emails/mo | $75 + $79 = $154/mo | ~$50–$60/mo VPS |
| 25k contacts, 200k emails/mo | $150 + $149 = $299/mo | Same VPS |
| 100k contacts, 500k emails/mo | $300 + $299 = $599/mo | Same VPS |
| Deliverability managed | ✓ (Mailgun) | You manage it |
| Self-hostable | ✗ | ✓ |
Prices subject to change — verify at Mailgun and ActiveCampaign official sites. The self-hosted option becomes cost-effective at >50k emails/month only if you manage deliverability successfully.
When this stack isn't right for you
- You send fewer than 50k emails/month — managed email services are cheaper after accounting for your operational time. The Billionmail deliverability management cost (warming, monitoring, bounce handling) is real.
- You need guaranteed inbox placement — managed SMTP providers (Postmark, Mailgun, SendGrid) have warm IP pools. A fresh dedicated IP takes 4–8 weeks to build reputation.
- You need CRM alongside marketing — add Twenty CRM to this stack. Twenty + Mautic + Billionmail on a 16 GB VPS combined peaks around 4 GB RAM.
- Mautic's funding situation is a dealbreaker — see Listmonk as a maintained newsletter-focused alternative.
- Billionmail's early-stage maturity is a dealbreaker — see Postal (MIT, v3.3.6, 2026-04-28, more mature) as an SMTP alternative.
Exit strategy
# Export Mautic contacts (CSV)
# Mautic UI → Contacts → All → Export
# Database dumps
docker compose exec postgres pg_dump -U billionmail billionmail > billionmail_db.sql
docker compose exec mariadb mysqldump -u mautic -p mautic > mautic_db.sql
# Application data
docker compose exec billionmail tar -czf /tmp/bm_data.tar.gz /app/data
docker cp billionmail:/tmp/bm_data.tar.gz ./
Billionmail can POST bounce notifications to a webhook URL. Configure the webhook in Billionmail admin → Settings → Bounce Webhook → `https://mautic.yourdomain.com/mailer/webhook`. Mautic's built-in webhook handler processes the notification and marks the contact as 'do not contact' automatically. Hard bounces are processed immediately; soft bounces are tracked across multiple sends before marking.
Yes. Billionmail supports multiple sending domains, each with its own DKIM key and sending configuration. In the Billionmail admin panel, add additional domains under Settings → Domains. Each domain gets its own DKIM public key to add to DNS. Mautic can be configured to send from different domains per campaign by adjusting the From Email in the campaign settings.
Check the blocklist at mxtoolbox.com/blacklists.aspx. Most blocklists have a self-service delisting process — submit a delisting request and explain the corrective action taken (e.g., removed bad addresses, fixed consent process). While the IP is blocklisted, you can add a secondary sending domain or temporarily route through a managed SMTP service by updating MAUTIC_MAILER_DSN in .env. This is one of the operational risks of self-hosted email.
Postal (MIT, v3.3.6, 2026-04-28) has a longer track record, more active maintenance history, and better community documentation. Billionmail (AGPL-3.0, v4.9, 2025-12-11) has a newer, more polished UI but irregular commit cadence for an early-stage project. For production-critical transactional email, Postal's maintenance track record is stronger. Billionmail is interesting but warrants monitoring before long-term commitments.

Software Developer & Automation Specialist
I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.
