Skip to main content

Plausible + Formbricks + Mautic: Privacy-First Analytics-to-Nurture Stack (2026)

· 15 min read
Yassine El Haddad
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.

TL;DR

  • One docker-compose.yml: Plausible + Formbricks + Mautic + PostgreSQL 16 + ClickHouse + MariaDB 10.11 + Redis + Caddy
  • Measured idle RAM: ~2.3 GB; peak: ~3.5 GB (500 Plausible events/min + 50 active surveys + Mautic sending a 2k campaign)
  • Minimum Liquid Web tier: 8 GB Managed VPS (~$30–$40/mo)
  • GA4 (free) + Typeform ($50/mo) + ActiveCampaign ($49/mo) = $99/mo; this stack: ~$30/mo on your own server

Most SaaS teams have three separate tools that should talk to each other but don't: an analytics platform, a user feedback tool, and an email automation platform. The data lives in three silos. You see a traffic spike in GA4, you have NPS responses in Typeform, and you have email segments in ActiveCampaign — but you can't close the loop automatically. This stack connects all three: Plausible fires a goal event when a user reaches a key page, Formbricks uses that signal to trigger an NPS survey, and a Formbricks webhook pushes the response straight into Mautic where a campaign branches on score.

The other problem with the SaaS version of this stack: GA4 requires cookie consent banners in the EU, Typeform stores your respondents' data on Typeform's infrastructure, and ActiveCampaign holds your contact list. Self-hosting puts all three under your control, eliminates the consent banner requirement (Plausible is cookieless by design), and costs a third of the combined SaaS bill.

Read the individual tool guides before deploying the combo:

:::caution Mautic funding notice Mautic's 2026 community fundraiser reached only ~$2,700 of its $60,000 goal as of this writing. The software works and is actively maintained, but weigh the project's financial health against your long-term dependency on it. :::

What you get

ToolRoleReplaces
Plausible AnalyticsCookieless page views, traffic sources, custom goalsGoogle Analytics 4
FormbricksIn-product NPS / CSAT / PMF surveys triggered by behaviorTypeform
MauticContact segmentation + email drip sequencesActiveCampaign
PostgreSQL 16Shared: Plausible metadata + Formbricks app data
ClickHouse 24Plausible event storage (time-series, write-heavy)
MariaDB 10.11Mautic app database (Mautic works best on MySQL/MariaDB)
Redis 7Shared: Plausible cache + Formbricks job queue
CaddyTLS termination, three virtual hosts

What's not included: transactional email delivery — you need an external SMTP provider (Postmark, Mailgun, or Billionmail if you want to self-host that too). Live chat and CRM are also not included; see the Twenty CRM + Chatwoot guide for those.

Prerequisites

  • A Liquid Web 8 GB Managed VPS — verify current pricing
  • Docker Engine 25+ and Docker Compose V2
  • Three subdomains pointed at the VPS IP:
    • analytics.yourdomain.com → Plausible
    • forms.yourdomain.com → Formbricks
    • marketing.yourdomain.com → Mautic
  • An SMTP provider for Mautic outbound email
  • About 90 minutes

The complete docker-compose.yml

This single file runs all three apps plus their shared and dedicated infrastructure. PostgreSQL is shared between Plausible and Formbricks (separate databases). ClickHouse is Plausible-only. MariaDB is Mautic-only. Redis is shared between Plausible and Formbricks (separate key namespaces).

# plausible-formbricks-mautic-stack/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: ~2.3 GB RAM | Peak: ~3.5 GB
# (500 Plausible events/min + 50 active surveys + Mautic 2k send)

services:
# ─── Shared infrastructure ────────────────────────────────────────────────

postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- stack_net

# ─── Plausible-only: ClickHouse ───────────────────────────────────────────

clickhouse:
image: clickhouse/clickhouse-server:24-alpine
restart: unless-stopped
volumes:
- clickhouse_data:/var/lib/clickhouse
- ./clickhouse/config.xml:/etc/clickhouse-server/config.d/config.xml:ro
- ./clickhouse/user-config.xml:/etc/clickhouse-server/users.d/user-config.xml:ro
ulimits:
nofile:
soft: 262144
hard: 262144
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8123/ping | grep -q ok"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

# ─── Mautic-only: MariaDB ─────────────────────────────────────────────────

mariadb:
image: mariadb:10.11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MYSQL_DATABASE: mautic
MYSQL_USER: mautic
MYSQL_PASSWORD: ${MAUTIC_DB_PASSWORD}
volumes:
- mariadb_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net

# ─── Plausible Analytics ──────────────────────────────────────────────────

plausible:
image: ghcr.io/plausible/community-edition:${PLAUSIBLE_VERSION:-v0.15.0}
restart: unless-stopped
environment:
BASE_URL: https://${PLAUSIBLE_DOMAIN}
SECRET_KEY_BASE: ${PLAUSIBLE_SECRET_KEY_BASE}
DATABASE_URL: postgresql://plausible:${PLAUSIBLE_DB_PASSWORD}@postgres:5432/plausible_db
CLICKHOUSE_DATABASE_URL: http://clickhouse:8123/plausible_events_db
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
MAILER_EMAIL: ${PLAUSIBLE_FROM_EMAIL}
SMTP_HOST_ADDR: ${SMTP_ADDRESS}
SMTP_HOST_PORT: ${SMTP_PORT:-587}
SMTP_USER_NAME: ${SMTP_USERNAME}
SMTP_USER_PWD: ${SMTP_PASSWORD}
SMTP_HOST_SSL_ENABLED: "false"
DISABLE_REGISTRATION: "true"
depends_on:
postgres:
condition: service_healthy
clickhouse:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8000/api/health | grep -q ok"]
interval: 15s
timeout: 5s
retries: 5

# ─── Formbricks ───────────────────────────────────────────────────────────

formbricks:
image: ghcr.io/formbricks/formbricks:${FORMBRICKS_VERSION:-v2.5.0}
restart: unless-stopped
environment:
WEBAPP_URL: https://${FORMBRICKS_DOMAIN}
DATABASE_URL: postgresql://formbricks:${FORMBRICKS_DB_PASSWORD}@postgres:5432/formbricks
NEXTAUTH_SECRET: ${FORMBRICKS_NEXTAUTH_SECRET}
NEXTAUTH_URL: https://${FORMBRICKS_DOMAIN}
ENCRYPTION_KEY: ${FORMBRICKS_ENCRYPTION_KEY}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/1
MAIL_FROM: ${FORMBRICKS_FROM_EMAIL}
SMTP_HOST: ${SMTP_ADDRESS}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USER: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_SECURE_ENABLED: "0"
# Set to "1" to require email verification; "0" for self-hosted convenience
EMAIL_VERIFICATION_DISABLED: "1"
depends_on:
postgres:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health | grep -q ok"]
interval: 15s
timeout: 5s
retries: 5

# ─── Mautic ───────────────────────────────────────────────────────────────

mautic:
image: mautic/mautic:${MAUTIC_VERSION:-5.1.0}-apache
restart: unless-stopped
environment:
MAUTIC_DB_HOST: mariadb
MAUTIC_DB_USER: mautic
MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD}
MAUTIC_DB_NAME: mautic
MAUTIC_RUN_CRON_JOBS: "true"
MAUTIC_TRUSTED_PROXIES: '["0.0.0.0/0"]'
PHP_INI_DATE_TIMEZONE: UTC
volumes:
- mautic_data:/var/www/html
depends_on:
mariadb:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/mtc.js || exit 1"]
interval: 20s
timeout: 10s
retries: 5

# ─── Caddy TLS proxy ──────────────────────────────────────────────────────

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:
plausible:
condition: service_healthy
formbricks:
condition: service_healthy
mautic:
condition: service_healthy
networks:
- stack_net

volumes:
postgres_data:
redis_data:
clickhouse_data:
mariadb_data:
mautic_data:
caddy_data:
caddy_config:

networks:
stack_net:
driver: bridge

Database init script

Create scripts/init-db.sh to provision separate PostgreSQL databases for Plausible and Formbricks:

#!/bin/bash
# scripts/init-db.sh — runs on first postgres start only
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE USER plausible WITH PASSWORD '${PLAUSIBLE_DB_PASSWORD}';
CREATE DATABASE plausible_db OWNER plausible;

CREATE USER formbricks WITH PASSWORD '${FORMBRICKS_DB_PASSWORD}';
CREATE DATABASE formbricks OWNER formbricks;
EOSQL

Make it executable: chmod +x scripts/init-db.sh

ClickHouse configuration

Create two minimal config files. Plausible connects to ClickHouse over HTTP on port 8123.

clickhouse/config.xml:

<clickhouse>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<query_log remove="remove"/>
<query_thread_log remove="remove"/>
</clickhouse>

clickhouse/user-config.xml:

<clickhouse>
<profiles>
<default>
<log_queries>0</log_queries>
<log_query_threads>0</log_query_threads>
</default>
</profiles>
</clickhouse>

Caddyfile

Three virtual hosts, one Caddyfile:

# Caddyfile — Plausible + Formbricks + Mautic

# Plausible Analytics
analytics.yourdomain.com {
reverse_proxy plausible:8000

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
}

# Formbricks surveys
forms.yourdomain.com {
reverse_proxy formbricks:3000

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Formbricks embeds in iframes; SAMEORIGIN is too strict here
X-Content-Type-Options nosniff
}
}

# Mautic email marketing
marketing.yourdomain.com {
reverse_proxy mautic:80

encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}

.env file

# .env — never commit this file

# App versions
PLAUSIBLE_VERSION=v0.15.0
FORMBRICKS_VERSION=v2.5.0
MAUTIC_VERSION=5.1.0

# Domains
PLAUSIBLE_DOMAIN=analytics.yourdomain.com
FORMBRICKS_DOMAIN=forms.yourdomain.com
MAUTIC_DOMAIN=marketing.yourdomain.com

# PostgreSQL superuser (used by init-db.sh)
POSTGRES_PASSWORD=change-me-postgres-superuser-password

# Plausible database user
PLAUSIBLE_DB_PASSWORD=change-me-plausible-db-password

# Plausible secret — generate with: openssl rand -hex 64
PLAUSIBLE_SECRET_KEY_BASE=

# Formbricks database user
FORMBRICKS_DB_PASSWORD=change-me-formbricks-db-password

# Formbricks secrets — generate each with: openssl rand -hex 32
FORMBRICKS_NEXTAUTH_SECRET=
FORMBRICKS_ENCRYPTION_KEY=

# MariaDB (Mautic)
MARIADB_ROOT_PASSWORD=change-me-mariadb-root-password
MAUTIC_DB_PASSWORD=change-me-mautic-db-password

# Redis (shared)
REDIS_PASSWORD=change-me-redis-password

# SMTP — used by all three apps
SMTP_ADDRESS=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USERNAME=your-postmark-server-token
SMTP_PASSWORD=your-postmark-server-token

# From addresses
PLAUSIBLE_FROM_EMAIL=analytics@yourdomain.com
FORMBRICKS_FROM_EMAIL=forms@yourdomain.com

Generate secrets in one shot:

for var in PLAUSIBLE_SECRET_KEY_BASE FORMBRICKS_NEXTAUTH_SECRET FORMBRICKS_ENCRYPTION_KEY; do
echo "${var}=$(openssl rand -hex 32)"
done

First-run setup

Step 1 of 8: Point DNS

analytics.yourdomain.com. A <your-vps-ip>
forms.yourdomain.com. A <your-vps-ip>
marketing.yourdomain.com. A <your-vps-ip>

Wait for DNS to propagate before continuing (check with dig analytics.yourdomain.com).

Step 2 of 8: Start databases first

docker compose up -d postgres clickhouse mariadb redis
# Wait for all four to report healthy — usually under 60 seconds
docker compose ps

Step 3 of 8: Run Plausible migrations

docker compose run --rm plausible /app/bin/migrate

Step 4 of 8: Run Formbricks migrations

Formbricks runs Prisma migrations automatically on startup. Start it once to let it self-migrate, then confirm it reached a healthy state:

docker compose up -d formbricks
docker compose logs -f formbricks # watch for "Ready on http://0.0.0.0:3000"

Step 5 of 8: Start Mautic

docker compose up -d mautic
docker compose logs -f mautic # watch for Apache to start; first boot takes 60–90s

Mautic runs its own web installer on first boot. Visit https://marketing.yourdomain.com/installer to complete database setup through the UI.

Step 6 of 8: Start Caddy and the full stack

docker compose up -d
docker compose ps # all containers should be healthy

Step 7 of 8: Create your Plausible site

  1. Register at https://analytics.yourdomain.com (first registration becomes admin)
  2. Add your website — enter the domain you want to track (e.g., yourdomain.com)
  3. Copy the tracking snippet — Plausible displays it after site creation

Embed the snippet in your site's <head>:

<script defer data-domain="yourdomain.com"
src="https://analytics.yourdomain.com/js/script.js"></script>

Step 8 of 8: Create your Formbricks environment

  1. Register at https://forms.yourdomain.com
  2. Create a new Product → note the Environment ID shown in Settings → Setup
  3. Embed the Formbricks JS SDK in your app:
<script type="module">
import Formbricks from "https://forms.yourdomain.com/api/packages/js";
Formbricks.init({
environmentId: "YOUR_ENVIRONMENT_ID",
apiHost: "https://forms.yourdomain.com",
});
</script>

Connecting the tools

This is the part that makes the stack a loop rather than three isolated tools.

1. Define a Plausible custom goal

In Plausible: Site Settings → Goals → Add Goal. Choose Custom event and name it Activated (or whatever matches your app's key milestone). Fire this event from your frontend when the user reaches that milestone:

// fire when user completes onboarding, hits a paywall, etc.
window.plausible("Activated");

Plausible records the event and shows it in the Goals report. You'll use this as the trigger signal for Formbricks.

2. Trigger the Formbricks NPS survey by page view count

In Formbricks: Surveys → New Survey → NPS. Under Targeting:

  • Set Trigger to pageView with a minimum count of 5 (show after the user has seen 5 pages)

Or combine with a JavaScript trigger that fires after the Plausible Activated event:

window.plausible("Activated", {
callback: () => Formbricks.track("user-activated"),
});

In Formbricks, set the trigger to the custom action user-activated. This ensures the NPS survey only appears to users who have meaningfully engaged, not first-time visitors.

3. Send Formbricks responses to Mautic via webhook

In Mautic — enable API and create an API user:

  1. Mautic → Settings (gear icon) → Configuration → API Settings → Enable API: On
  2. Mautic → Settings → API Credentials → New → Basic Auth → save the username and password

In Formbricks — add a webhook integration:

  1. Formbricks → Integrations → Webhooks → Add Webhook
  2. URL: https://marketing.yourdomain.com/api/contacts/new
  3. Trigger: On response created
  4. Authentication: Basic Auth → enter your Mautic API username and password

Formbricks will POST the survey response to Mautic's contact creation endpoint. Map the survey fields in the webhook payload:

{
"email": "{{question.email}}",
"firstName": "{{question.firstName}}",
"tags": "nps-{{question.npsScore}}"
}

In Mautic — create segments:

  1. Contacts → Segments → New:
    • Segment: NPS Promoters — filter: Tag contains nps-9 OR nps-10
    • Segment: NPS Detractors — filter: Tag contains nps-0 through nps-6

In Mautic — create campaigns:

  • Promoter campaign: Source = NPS Promoters → Wait 1 day → Send "Would you refer us?" email with a referral link
  • Detractor campaign: Source = NPS Detractors → Wait 1 hour → Send "How can we improve?" email with a reply-to address or Typeform link (or another Formbricks survey)

Test the full flow end-to-end:

# Simulate a Formbricks webhook to Mautic
curl -X POST https://marketing.yourdomain.com/api/contacts/new \
-u YOUR_API_USER:YOUR_API_PASS \
-d 'email=test@example.com&firstName=Test&tags=nps-9'

# Expected response: {"contact":{"id":1,"email":"test@example.com",...}}

Troubleshooting

Plausible shows no data after embedding the script

Check that the domain entered in Plausible's site settings exactly matches your actual website domain — no www vs non-www mismatch, no trailing slash. Also verify that no ad blocker is active in your test browser. Open DevTools → Network tab → filter by analytics.yourdomain.com — the script.js request should return 200 and a pageview hit should appear within a few seconds of page load.

Formbricks survey doesn't appear on the website

The environmentId in the Formbricks JS SDK must exactly match the one shown in Formbricks → Settings → Setup (they look like clxxxxxxxxxxxx). Also check that WEBAPP_URL in your .env matches the URL you actually browse to — Formbricks uses this value for CORS validation and rejects requests from mismatched origins.

Mautic webhook receiver returns 400 on Formbricks submission

Mautic's contact creation endpoint is /api/contacts/new. Confirm the Formbricks webhook payload includes an email field (required) and uses firstName not firstname (case-sensitive). Test the endpoint directly:

curl -X POST https://marketing.yourdomain.com/api/contacts/new \
-u API_USER:API_PASS \
-d 'email=test@example.com&firstName=Test'

A 401 means API auth is misconfigured in Mautic. A 400 with a validation message usually means a missing or misspelled required field.

Runtime footprint

Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM) — equivalent to a Liquid Web 8 GB Managed VPS.

ServiceIdle RAMPeak RAM
plausible200 MB350 MB
clickhouse400 MB700 MB
formbricks530 MB850 MB
mautic512 MB900 MB
postgres (shared)300 MB480 MB
mariadb256 MB400 MB
redis (shared)50 MB80 MB
caddy15 MB20 MB
Total~2,263 MB~3,780 MB

An 8 GB Managed VPS provides approximately 5.9 GB usable after OS overhead. This stack uses ~2.3 GB idle and peaks at ~3.8 GB, leaving roughly 2 GB of headroom for buffer cache and traffic spikes.

Cost comparison

GA4 + Typeform + ActiveCampaignThis stack on Liquid Web
Monthly cost$0 + $50 + $49 = $99/mo~$30/mo VPS
Pageview limitGA4 unlimited / Typeform 1k responsesUnlimited
Cookie consent requiredYes (GA4)No (Plausible is cookieless)
Data ownershipGoogle / Typeform / ActiveCampaignYours
NPS surveys✓ Typeform✓ Formbricks
Email automation✓ ActiveCampaign✓ Mautic
Self-hostable

Prices subject to change — verify current Typeform pricing at typeform.com/pricing, ActiveCampaign at activecampaign.com/pricing, and Liquid Web at liquidweb.com/vps-hosting/managed-vps/.

When this stack isn't right for you

  • You need real-time survey branching and conditional logic — Formbricks supports branching, but it is less mature than Typeform's Logic Jump. If your surveys have many branches, test Formbricks thoroughly before committing.
  • You're sending more than 50k emails per month — Mautic can handle volume, but you'll need a reputable SMTP relay (Postmark, SendGrid, or AWS SES) and proper SPF/DKIM/DMARC setup. Mautic itself doesn't deliver email; your SMTP provider does.
  • You need GA4 audience sync with Google Ads — Plausible has no integration with Google Ads. If paid search attribution and remarketing audiences are critical, GA4 is the better fit. You can run both simultaneously: keep GA4 for Ads, use Plausible for everything else.
  • Mautic's funding situation is a dealbreaker — The 2026 fundraising shortfall is a real signal. If you need a commercially backed email automation platform, consider Brevo (freemium, GDPR-friendly) as a managed alternative to Mautic.
  • Your team is non-technical — Mautic's campaign builder is powerful but has a steeper learning curve than ActiveCampaign. Budget time for onboarding your marketing team.
Frequently Asked Questions

Yes — that is exactly what this stack does. Each app gets its own PostgreSQL database (plausible_db and formbricks) within the same container, created by the init-db.sh script on first boot. The apps never touch each other's tables. The init script uses separate PostgreSQL users with passwords scoped to their respective databases.

Plausible manages its own ClickHouse schema through internal migrations that run automatically on startup. Before upgrading, take a ClickHouse volume backup: `docker compose exec clickhouse clickhouse-client --query "BACKUP DATABASE plausible_events_db TO Disk('backups', 'pre-upgrade.zip')"`. Then update PLAUSIBLE_VERSION in your .env and run `docker compose pull && docker compose up -d plausible`. Watch the logs for migration completion before sending traffic.

Yes. The Plausible tracking script and Formbricks SDK are both loaded from their respective subdomains (analytics.yourdomain.com and forms.yourdomain.com) and work on any website that can load external JavaScript — regardless of where the site itself is hosted. Only Mautic needs to reach your SMTP provider outbound; it has no inbound dependency on your website server.

Photo of Yassine El Haddad

Yassine El Haddad

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.