Plausible + Formbricks + Mautic: Privacy-First Analytics-to-Nurture Stack (2026)
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:
- Self-Host Plausible Analytics — Plausible architecture, ClickHouse, and standalone setup
- Self-Host Formbricks — Formbricks SDK, webhook setup, and standalone deployment
- Self-Host Mautic — Mautic campaign builder, MariaDB, and standalone setup
:::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
| Tool | Role | Replaces |
|---|---|---|
| Plausible Analytics | Cookieless page views, traffic sources, custom goals | Google Analytics 4 |
| Formbricks | In-product NPS / CSAT / PMF surveys triggered by behavior | Typeform |
| Mautic | Contact segmentation + email drip sequences | ActiveCampaign |
| PostgreSQL 16 | Shared: Plausible metadata + Formbricks app data | — |
| ClickHouse 24 | Plausible event storage (time-series, write-heavy) | — |
| MariaDB 10.11 | Mautic app database (Mautic works best on MySQL/MariaDB) | — |
| Redis 7 | Shared: Plausible cache + Formbricks job queue | — |
| Caddy | TLS 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→ Plausibleforms.yourdomain.com→ Formbricksmarketing.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
- Register at
https://analytics.yourdomain.com(first registration becomes admin) - Add your website — enter the domain you want to track (e.g.,
yourdomain.com) - 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
- Register at
https://forms.yourdomain.com - Create a new Product → note the Environment ID shown in Settings → Setup
- 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
pageViewwith 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:
- Mautic → Settings (gear icon) → Configuration → API Settings → Enable API: On
- Mautic → Settings → API Credentials → New → Basic Auth → save the username and password
In Formbricks — add a webhook integration:
- Formbricks → Integrations → Webhooks → Add Webhook
- URL:
https://marketing.yourdomain.com/api/contacts/new - Trigger: On response created
- 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:
- Contacts → Segments → New:
- Segment:
NPS Promoters— filter: Tag containsnps-9ORnps-10 - Segment:
NPS Detractors— filter: Tag containsnps-0throughnps-6
- Segment:
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.
| Service | Idle RAM | Peak RAM |
|---|---|---|
| plausible | 200 MB | 350 MB |
| clickhouse | 400 MB | 700 MB |
| formbricks | 530 MB | 850 MB |
| mautic | 512 MB | 900 MB |
| postgres (shared) | 300 MB | 480 MB |
| mariadb | 256 MB | 400 MB |
| redis (shared) | 50 MB | 80 MB |
| caddy | 15 MB | 20 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 + ActiveCampaign | This stack on Liquid Web | |
|---|---|---|
| Monthly cost | $0 + $50 + $49 = $99/mo | ~$30/mo VPS |
| Pageview limit | GA4 unlimited / Typeform 1k responses | Unlimited |
| Cookie consent required | Yes (GA4) | No (Plausible is cookieless) |
| Data ownership | Google / Typeform / ActiveCampaign | Yours |
| 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.
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.

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.
