Skip to main content
Technical Blueprints

Mautic Self-Hosted Marketing Automation: My Honest Guide

How I deploy Mautic for clients who refuse to ship lead data to HubSpot, plus the SMTP traps that make most self-hosted setups quietly fail.

Published Updated 10 min read

I deploy Mautic for the same reason every time: a client wants HubSpot-grade marketing automation but cannot, for compliance or budget reasons, ship their lead database to a US SaaS. Mautic self-hosted marketing automation is the boring, working answer to that question. I have rolled it out for around twelve clients now, mostly EU agencies and B2B operators with strict GDPR posture.

This post is the stack I ship: Mautic plus MariaDB on a 4GB VPS, fronted by a real SMTP relay, with cron jobs and the trusted-proxy header set the way they need to be set. About 90 minutes from a fresh server to a working dashboard with a sender domain that doesn’t immediately land in spam.

A warning before you start. Mautic is not a “set it up and forget it” tool. It rewards operators who understand drip campaigns, segmentation logic, and the unglamorous work of email warm-up. If nobody on your team owns marketing automation as a discipline, you’ll be paying VPS bills for a tool nobody uses.

When Mautic actually beats HubSpot or ActiveCampaign

The honest answer: most of the time, it doesn’t. HubSpot Marketing Hub and ActiveCampaign have better UX, better integrations, and a support team that picks up the phone. If you can stomach their pricing and their data-residency posture, use them.

Mautic wins on three specific scenarios I keep running into:

1. Data residency and GDPR compliance. I work with healthcare adjacent clients, EU public-sector partners, and a few legal-services firms. None of them can ship contact records to a US-based vendor without a Data Protection Impact Assessment that takes six months to clear. Self-hosted Mautic on a Hetzner VPS in Falkenstein bypasses that conversation. The DPO signs off in an afternoon.

2. Per-contact pricing breaks the budget. ActiveCampaign starts billing aggressively past 25,000 contacts. HubSpot’s Marketing Pro plan jumps from €792/month to €3,300/month at the 10,000 marketing-contact threshold. For a B2C client with 200,000 contacts and modest send volume, Mautic on a €30/month VPS and a €15/month Postmark plan beats those numbers by an order of magnitude.

3. Custom integrations vendors won’t build. Mautic exposes a real REST API, has a plugin architecture, and lets you write campaign actions in PHP if you must. I have one client whose CRM is a 2008-era custom Symfony app. Mautic talks to it. HubSpot would have required a six-figure integration project.

Outside those three scenarios, I tell clients to use a hosted product and get back to running their business.

The Mautic deployment stack

Two containers do the entire job: Mautic itself and a MariaDB instance for storage. I deliberately keep this small. Mautic ships a default Apache image that handles PHP-FPM, the cron daemon, and the web server in one process. Splitting them up gains you nothing for an agency-scale workload and adds three more things to babysit.

Prerequisites

A 4GB VPS at minimum, with 2 vCPUs and 40GB of disk. I have run Mautic on a 2GB box for testing, but the first time you build a segment over 50,000 contacts, the PHP worker hits the memory limit and the recalculation fails silently. Pay for the 4GB plan.

You also need a properly hardened server before any of this runs. If you haven’t done that yet, start with Linux server security fundamentals and come back. Deploying Mautic on an unsecured box is its own category of mistake.

DNS access for your sender domain is non-negotiable. You’ll be adding SPF, DKIM, and DMARC records before you send a single test email. If you can’t edit DNS, stop here.

Docker engine

Install Docker from the official Docker repository, following the official installation guide for Ubuntu. Don’t use the version in apt’s default repo, and don’t run a “convenient” curl-pipe-bash script you found on a forum. The official repository is the only correct source.

Docker Compose file

Here’s the working docker-compose.yml I ship. Change every password before you bring it up. I am being literal about that.

version: "3.9"

services:
  database:
    image: mariadb:11
    container_name: mautic-db
    environment:
      MYSQL_ROOT_PASSWORD: change-me-to-a-real-password
      MYSQL_DATABASE: mautic
      MYSQL_USER: mautic
      MYSQL_PASSWORD: also-change-this
    volumes:
      - database:/var/lib/mysql:rw
    restart: always
    networks:
      - mauticnet
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --sql-mode=""

  mautic:
    container_name: mautic
    image: mautic/mautic:5-apache
    volumes:
      - mautic_data:/var/www/html:rw
    environment:
      - MAUTIC_DB_HOST=database
      - MAUTIC_DB_USER=mautic
      - MAUTIC_DB_PASSWORD=also-change-this
      - MAUTIC_DB_NAME=mautic
      - MAUTIC_DB_TABLE_PREFIX=mautic_
      - MAUTIC_RUN_CRON_JOBS=true
      - MAUTIC_TRUSTED_PROXIES=["0.0.0.0/0"]
      - PHP_INI_DATE_TIMEZONE=Europe/Berlin
      - PHP_MEMORY_LIMIT=512M
      - PHP_MAX_UPLOAD=50M
      - PHP_MAX_EXECUTION_TIME=300
    restart: always
    depends_on:
      - database
    networks:
      - mauticnet
    ports:
      - "127.0.0.1:8888:80"

networks:
  mauticnet:

volumes:
  database:
  mautic_data:

A few notes on what changed from the upstream sample, because every change is deliberate.

The MariaDB image is pinned to major version 11 instead of latest. latest will eventually pull a new major release and silently break replication semantics. Pin majors, review minors before bumping.

The Mautic image is the 5-apache tag. Mautic 5.x is the current major; I no longer ship version 4 to new deployments.

The MariaDB port is not published to the host. There is no reason to expose a database to the public internet — container traffic happens over the mauticnet network without ever touching 0.0.0.0.

Mautic’s port 80 binds to 127.0.0.1:8888 instead of 0.0.0.0:8888. A reverse proxy (Nginx Proxy Manager or Caddy) terminates TLS on 443 and forwards to 127.0.0.1:8888. Skip the proxy and you’re running marketing automation over plain HTTP, which is bad on every axis.

MAUTIC_TRUSTED_PROXIES is set so Mautic respects X-Forwarded-For from the reverse proxy. Without this, every visitor logs as the proxy’s IP and your geographic segments become useless.

PHP_MEMORY_LIMIT is bumped to 512M. The 256M default fails on segment rebuilds past about 30,000 contacts.

Bring it up:

docker compose up -d
docker compose logs -f mautic

The first boot takes around 90 seconds while the database initializes and Mautic runs its install routine. Watch the logs until you see Apache start serving, then visit the configured domain. The initial setup wizard will ask for the database connection (use the values from the compose file) and an admin user.

Reverse proxy in front

I use Nginx Proxy Manager for every Docker stack I deploy. If you followed my Uptime Kuma deployment guide, you already have NPM running on the same box or on a sibling VPS.

Add a Proxy Host pointing mautic.yourdomain.com to http://<host-ip>:8888, request a Let’s Encrypt certificate, tick “Force SSL” and “HTTP/2 Support”, done.

If you’d rather use Caddy or a hand-rolled Nginx config, the only requirements are: terminate TLS, forward to port 8888 on the Mautic host, and pass X-Forwarded-For and X-Forwarded-Proto headers correctly.

The SMTP relay reality

Here’s the part the upstream install guide is too polite about: Mautic on its own does not solve email deliverability. It is a sender, not a relay. Point it at your domain’s mail server, blast 5,000 emails, and half will land in spam while the other half get rate-limited. By the time you notice, your domain reputation is already damaged.

The fix is the same one everyone in transactional email already knows: use a real SMTP provider.

I ship Mautic with one of three relays, depending on the client:

  • Postmark for clients who care about deliverability above all else and send under 1 million emails per month. Around €15-€60/month for typical agency volumes.
  • Mailgun for clients with higher volume or a need for EU-region servers. Mailgun’s EU plan keeps sending logs in Frankfurt, which keeps the DPO conversation short.
  • Amazon SES for clients who already live in AWS and want pay-per-send pricing. Cheapest option if you can stomach getting out of the SES sandbox before anything works.

Configure the relay credentials inside Mautic at Settings → Configuration → Email Settings. Use the relay’s SMTP credentials, port 587, TLS encryption. Set the “From” address to a domain you control and have authenticated.

Then set up SPF, DKIM, and DMARC on the sending domain before you send a campaign. The exact records vary by provider; each of the three I named has a setup wizard. Do not click “send” until those records are live and verified.

Cron is the silent killer

Mautic relies on background jobs for almost everything that matters: segment recalculation, scheduled campaign sends, lead score updates, email open processing. The default Docker image runs these via an internal cron daemon when MAUTIC_RUN_CRON_JOBS=true, which is why I set that explicitly above.

Verify the cron jobs are firing on day one:

docker exec -it mautic tail -f /var/log/cron.log

You should see entries every few minutes for mautic:segments:update, mautic:campaigns:update, and mautic:campaigns:trigger. If you don’t, campaigns won’t run, segments won’t update, and you’ll figure it out three weeks later when a client asks why their nurture sequence stopped.

If you turned the internal cron off (some operators prefer host-level crontab), you’ll need entries like this on the host:

*/5 * * * * docker exec mautic php /var/www/html/bin/console mautic:segments:update
*/5 * * * * docker exec mautic php /var/www/html/bin/console mautic:campaigns:update
*/5 * * * * docker exec mautic php /var/www/html/bin/console mautic:campaigns:trigger
*/15 * * * * docker exec mautic php /var/www/html/bin/console mautic:emails:send

Either approach works. Pick one and document which. Half the broken Mautic deployments I’ve audited had the internal cron disabled and no replacement on the host.

Maintenance that nobody warns you about

A self-hosted Mautic is not a fire-and-forget system. Plan for these tasks:

Backups. The MariaDB volume holds every contact, every campaign, every email click. Back it up nightly with restic or borgbackup, encrypt the backup, ship it offsite. Test the restore once a quarter. I have audited Mautic instances where backups had been “running” for a year and the restore failed on the first attempt because nobody had tested it.

Updates. Mautic ships minor releases roughly monthly. Read the upgrade notes before you bump the image tag. Major versions sometimes include database migrations that take 20+ minutes on large installs, so schedule updates during a maintenance window, not at 2pm on a Tuesday.

Database growth. The page-hit and email-event tables grow fast. On a B2C instance with 200,000 contacts and active tracking, I see 10-15GB of database growth per year. Set up a quarterly retention job that drops events older than 18 months. The Mautic console has a mautic:cleanup:data command for exactly that.

SMTP credential rotation. Rotate per-server API tokens at least annually, more often if you’ve had team turnover. Update the credentials in Settings → Email Settings and send a test before you commit.

If the maintenance overhead sounds heavy, that’s because it is. Self-hosting marketing automation isn’t free; it’s just a different cost structure than SaaS pricing.

What I deliberately don’t bother with

A few things I’ve consciously left out of this baseline stack:

  • Multi-server Mautic clusters. Mautic supports horizontal scaling via Redis and a load balancer, but I have never seen an agency client whose workload justified it. Until you’re past 5 million contacts, vertical scaling on one VPS is simpler.
  • Custom Symfony plugins. Writing custom plugins requires real Symfony experience. Before you write one, ask whether an n8n workflow plus a webhook would do the same job in 20% of the time.
  • Pointing Mautic at your own mail server. Mautic can connect directly to Mailcow or any SMTP server you run. I’d still front it with Postmark or Mailgun. Transactional and marketing email have different reputation profiles, and mixing them on one IP is how good sender reputations die.

If a client needs a feature beyond this baseline, that’s a separate engagement.

Closing the loop

Self-hosted Mautic earns its place in three specific scenarios: GDPR-strict deployments, high-contact-volume budgets where SaaS pricing breaks, and integration projects where a hosted product won’t bend to a legacy CRM. Anywhere else, ActiveCampaign or Brevo will save you 20 hours a month.

If those scenarios match your situation, the stack above runs reliably across the dozen-or-so client deployments I currently operate. Two containers, one SMTP relay, a reverse proxy in front, and a calendar reminder for the quarterly cleanup. Pair it with Uptime Kuma so you find out when it breaks before the client does, and add it to whatever monitoring or workflow stack already lives in your operations tooling.

The stack isn’t glamorous. It’s just what works.

Watch on YouTube

Video walkthrough

Prefer the screen-recording version of this guide? Watch it on YouTube — opens in a new tab so the player only loads when you ask for it.

Frequently Asked Questions

Want this handled, not just understood?

Reading the playbook is one thing. Running it on production at 2am is another. If you'd rather have me run it for you, the door is open.

Apply for Access