Why self-host Mautic instead of paying for a marketing SaaS
Marketing SaaS prices the one thing you cannot control: the size of your list. Mailchimp bills by contact tier, and each tier also caps how much you may send — 10× your contact count on Essentials, 12× on Standard, 15× on Premium. Grow the list and both the invoice and the ceiling move against you. Brevo and ActiveCampaign price on the same axis.
Mautic removes that axis. You pay for the server and nothing else, and the bill is identical whether the database holds 5,000 contacts or 500,000: the entry ServOrbit VPS tier (2 vCPU, 4 GB RAM, 50 GB SSD, from 99 DH/month) carries a Mautic stack comfortably. Your data stays on your own infrastructure, where no SaaS host mines it for its own purposes. The platform is published under the GPL-3.0 and it is community-owned — no commercial vendor sits behind the core software, and the MAUTIC trademark is held by the Open Source Collective on behalf of the project. There is no free-versus-paid edition to work around either: Mautic is not a feature-gated build of a commercial product. A managed Mautic Cloud exists, but it hosts the same software you can run yourself.
What you get with self-hosted Mautic
- Visual campaign builder — conditional branches, time delays and event triggers.
- Lead scoring — points awarded for opens, clicks, page visits and form submissions.
- Dynamic segmentation — filters on any contact attribute, refreshed in real time.
- Landing pages and forms — hosted on your own domain, with no external dependency.
- Full REST API — integrate your apps, CRM and tools through webhooks.
- GPL-3.0 licence — no contact cap, no per-email fee, no proprietary retention clause.
Mautic 5 Docker architecture: four containers, one image
Mautic 5.x uses a multi-role architecture: a single Docker image (mautic/mautic:5.2.3-apache) is instantiated three times, each run under a different DOCKER_MAUTIC_ROLE:
- mautic_web: the Apache front end — it serves the installation wizard first, then the dashboard
- mautic_cron: runs the scheduled commands (segment rebuilds, campaign triggers, queued sends, cleanup, statistics)
- mautic_worker: consumes the asynchronous message queue (tracking hits, webhooks, notifications)
A fourth container, MySQL 8.0, stores contacts, campaigns and events. The three Mautic containers share one volume (mautic_data:/var/www/html) so the configuration written by the wizard is immediately visible to the cron and worker roles.
The cron role is the one people underestimate. Mautic sends nothing from the web request itself: a published campaign only fires because mautic:campaigns:trigger and mautic:emails:send run on a schedule. Stop that container and the interface still shows the campaign as published while nothing leaves the server — a failure that looks like a deliverability problem and is not one. PHP also lives inside the image: Mautic 5.2 runs on PHP 8.1 to 8.3, so whichever PHP the host carries is irrelevant here.
Installing Mautic on your ServOrbit VPS
Deploy from the Marketplace
In your ServOrbit client area, open Marketplace → Automation & Workflows → Mautic and click Deploy. The AWX playbook provisions Ubuntu 24.04, installs Docker, generates the MySQL password, starts the four containers and configures nginx as a reverse proxy for your domain. The Apache container is bound to the loopback interface only — it is never exposed directly to the internet.
Complete the Mautic installation wizard
Open your domain in a browser. Mautic redirects to the
/s/setupwizard. Enter the site URL, check the database connection (already pre-filled from the compose environment), create your administrator account and confirm. Get the site URL right on the first pass: tracking pixels, landing pages and unsubscribe links are all built from it.Configure your SMTP relay
Go to Settings → Email Settings and choose your SMTP provider: Amazon SES, Mailgun, Brevo (free tier of 300 emails per day) or Postmark. Two things to know about SES before you build a budget. First, its headline rate is $0.10 per 1,000 emails — not “under $0.10” — and it is billed PER RECIPIENT, not per message: a send to 100 recipients counts as 100 emails, which is exactly the regime of a Mautic campaign. Second, that rate belongs to the à-la-carte tier: a new SES account starts on the Essentials tier, at $0.16 per 1,000, and switching to à-la-carte is on you. Attachments are billed separately, by the gigabyte. Then click Test email and confirm delivery before you touch a campaign — a relay that fails quietly looks exactly like a campaign that has not started yet.
Create a segment and import your contacts
Under Segments → New Segment, define your filter criteria. Under Contacts → Import, upload your CSV and map the Email, First name and Last name columns onto Mautic fields. Enable double opt-in if your audience is based in the EU or in Morocco.
Build and publish your first campaign
Under Campaigns → New Campaign, drag a source (your segment) onto the canvas, add a Send email action, set a 24-hour delay, then add a conditional follow-up email. Publish, and let the worker and cron containers drain the send queue in the background.
SPF, DKIM and DMARC are not optional before your first send. Without them your campaigns land in spam at Gmail and Outlook — a problem the SMTP relay alone does not solve. Our [SPF/DKIM/DMARC guide](/blog/spf-dkim-dmarc-delivrabilite-emails-pro) walks through the DNS configuration step by step.
Use case: replacing Mailchimp for an SMB
A marketing agency running lists for 15 clients — 80,000 contacts in total — was invoiced 620 €/month by Mailchimp. Moving to self-hosted Mautic on a 4 GB RAM VPS replaced that invoice with the cost of a single entry-tier server, taking well over 7,000 € a year off the budget.
The migration took two days: CSV export from Mailchimp, import into Mautic, then rebuilding the automations in the campaign builder. The HTML email templates were imported from Mailchimp unchanged. Deliverability came out comparable because Mailgun was kept as the SMTP relay — sending reputation follows the relay, not the platform sitting in front of it.
One thing a CSV export does not carry is engagement history. Opens, clicks and page visits stay behind in Mailchimp, so every imported contact arrives in Mautic with a score of zero, and any segment built on past behaviour has to be re-earned from the first campaign onward. Plan that first send as a re-engagement wave rather than a like-for-like resume.
MySQL 8 compatibility: HTTP 500 on reports and ONLY_FULL_GROUP_BY mode
Some Mautic reports return HTTP 500 the moment you open them — typically a Page hits report that groups by URL and title and adds a COUNT column. The symptom is quiet: a blank report page, nothing in Mautic's application log, no explicit error anywhere in the interface.
Diagnosis. The ONLY_FULL_GROUP_BY SQL mode rejects SELECT queries whose non-aggregated columns are missing from the GROUP BY clause — the functional dependency rule of the SQL standard, which Mautic's report builder does not always satisfy. MySQL answers with error 1055, Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'mautic.ph.id'. Check the server with:
SHOW GLOBAL VARIABLES LIKE 'sql_mode';The mode has been on by default since MySQL 5.7.5, not since MySQL 8.0. Running 5.7 does not put you out of scope, and the ServOrbit Mautic template ships MySQL 8.0, so a Marketplace deployment is in scope by default. MariaDB leaves the mode out of its default sql_mode.
Upstream this was filed as mautic/mautic#17092 (reported on Mautic 7.1.3 against MySQL 8.4) and fixed by pull request #17099, merged onto the 7.x branch for the 7.2.0 milestone. The 5.2.x line this template runs does not carry that fix, so on this stack the workaround below is still what you need.
Workaround A — relax the mode, but write it somewhere that survives. Applied live, the statement below only affects connections opened afterwards, not the session that issues it:
SET GLOBAL sql_mode = (SELECT REPLACE(@@sql_mode, 'ONLY_FULL_GROUP_BY', ''));and the change is gone the moment the db container restarts, which restart: unless-stopped makes routine. Editing /etc/mysql/my.cnf does not help either: in the mysql:8.0 image that is not even the file the server reads (it is /etc/my.cnf, which pulls in /etc/mysql/conf.d/), and above all those files live inside the container while only /var/lib/mysql is a named volume — the edit is discarded on the next recreation. Two forms do persist — SET PERSIST sql_mode = ..., which MySQL writes to mysqld-auto.cnf in the data directory and therefore onto the mautic_db volume, or a command: --sql-mode=... line on the db service in the compose file, which is the form that survives a full redeploy of the stack.
Workaround B — run MariaDB instead. MariaDB does not enable ONLY_FULL_GROUP_BY by default, and Mautic 5 supports MariaDB 10.2 and above. ServOrbit ships MariaDB 10.11 and 11 images in other Marketplace templates, but the Mautic template itself is MySQL 8.0: swapping the engine is a change you make to this stack, not a template you pick at deploy time, and it means dumping and reloading the database.