Skip to content
Go back

Matomo Self-Hosted: When You Need Funnels

By SumGuy 15 min read
Matomo Self-Hosted: When You Need Funnels
Contents

Matomo: When Plausible Just Isn’t Enough

You’ve got Plausible running, privacy-first analytics feeding a little dashboard somewhere. Then someone asks: “Where are our users dropping off?”

Let’s kill a myth before it costs you a weekend. All three of these tools do funnels now. Umami has shipped a Funnel report since v2.3 and it’s free in the open-source app. Plausible added funnel analysis and gates it behind the Business plan, which starts at $19/month for 10k monthly pageviews (as of September 2026). Matomo has Funnels too, and on self-hosted Matomo the plugin costs €199 a year for 1 to 4 users.

So the reason to move to Matomo is not “it’s the only one with funnels.” That was true a few years ago. It isn’t now.

The reason is depth. Matomo keeps the raw visit log, not just aggregates, so a funnel step you find suspicious can be opened up into the individual sessions that produced it. It ships e-commerce tracking, goal conversion, custom dimensions, and segmentation in the free core, and sells heatmaps, session recording, cohorts, and form analytics as paid plugins on top. Plausible and Umami are deliberately aggregate-only. That’s a design choice they made for privacy and for the query cost, and it’s why their reports load instantly and Matomo’s need an archiver.

The catch is real. Matomo is more complex than Plausible, has more moving parts, more config, and a MySQL database that will grow. Go there when you actually need session-level answers and are willing to run a database for them.

Matomo vs. the Privacy Analytics Crowd

Prices below are the September 2026 checkout prices, taken from each vendor’s order page rather than a marketing table. Plausible bills monthly and scales by pageviews; Matomo plugins bill yearly and scale by user seats.

FeaturePlausibleUmamiMatomo On-Premise
FunnelsBusiness planFree€199/yr plugin
Goal trackingYesYesFree, core
E-commerceBusiness planRevenue reportFree, core
Custom dimensionsCustom propertiesEvent dataFree, core
SegmentationFiltersFiltersFree, core
Raw visit logNoNoFree, core
HeatmapsNoNoPaid plugin
Session recordingNoNoPaid plugin
Cohort analysisNoRetention reportPaid plugin
GDPR readyYesYesYes

Read that table twice before you migrate. If all you wanted was a funnel, Umami already gives you one for free and you can stop reading here. The Matomo column earns its money on the last four rows: raw visit logs, heatmaps, session recording, and cohorts.

The tradeoff is DevOps tax. You’re running a database, a PHP application server, and an archiver process. It’s not a two-container setup like Umami.

Why Matomo’s the Right Choice (Actually)

Matomo has been around since 2007 (originally as Piwik). It’s used by governments, banks, and enterprises who need analytics but can’t send data to Google. The core is GPLv3 and free, including e-commerce, goals, segmentation, and custom dimensions.

Be clear about the money, though. Matomo runs a Marketplace of premium plugins, and the ones people usually want from Matomo (Funnels, Heatmaps & Session Recording, Cohorts, Form Analytics, A/B Testing, Custom Reports) are paid, per year, per seat tier. On-Premise is not a crippled tier, but it is not the same feature set the Cloud Business customers get either. Price the plugins you need before you commit to the migration.

What you get for free is the privacy posture, and some of it is on by default:

And funnels. If you’re running a SaaS product, an e-commerce site, or anything with a user journey, funnels are where the money leaks show up. “Sign up, verify email, add payment, first purchase”: watch where people bail. That’s how you fix your conversion rate.

Docker Compose: Get Matomo Running in 10 Minutes

Here’s a Compose setup that survives contact with production. Pin the tags: matomo:latest moves under you on a docker compose pull, and Matomo’s schema migrations run on first request after an upgrade.

compose.yaml
services:
matomo:
image: matomo:5.13-apache
container_name: matomo
restart: unless-stopped
environment:
MATOMO_DATABASE_HOST: db
MATOMO_DATABASE_USERNAME: matomo
MATOMO_DATABASE_PASSWORD: ${DB_PASSWORD}
MATOMO_DATABASE_NAME: matomo
MATOMO_DATABASE_ADAPTER: mysql
volumes:
- ./matomo-data:/var/www/html
- ./matomo-php.ini:/usr/local/etc/php/conf.d/matomo.ini:ro
ports:
- "127.0.0.1:8080:80"
depends_on:
- db
networks:
- matomo-net
db:
image: mysql:8.0
container_name: matomo-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: matomo
MYSQL_USER: matomo
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- ./db-data:/var/lib/mysql
networks:
- matomo-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 3
archiver:
image: matomo:5.13-apache
container_name: matomo-archiver
restart: unless-stopped
environment:
MATOMO_DATABASE_HOST: db
MATOMO_DATABASE_USERNAME: matomo
MATOMO_DATABASE_PASSWORD: ${DB_PASSWORD}
MATOMO_DATABASE_NAME: matomo
volumes:
- ./matomo-data:/var/www/html
depends_on:
- db
networks:
- matomo-net
command: /bin/bash -c "while true; do php /var/www/html/console core:archive --url=http://matomo/index.php; sleep 3600; done"
networks:
matomo-net:
driver: bridge

No top-level volumes: block, because both services use bind mounts (./matomo-data, ./db-data). Declaring named volumes you never reference creates two orphaned Docker volumes and confuses the next person to read the file.

And the .env file:

DB_PASSWORD=your_secure_db_password_here
DB_ROOT_PASSWORD=your_secure_root_password_here

PHP config (matomo-php.ini):

memory_limit = 512M
max_execution_time = 300
upload_max_filesize = 256M
post_max_size = 256M

Spin it up:

Terminal window
docker compose up -d

Head to http://localhost:8080 and walk through the setup wizard. It’ll ask for your database credentials (all set from env vars), then create a user account for you, then ask you to add your first site.

The archiver service runs once an hour and pre-computes your reports. But adding the container is only half the job, and this is the step every Matomo tutorial skips.

Matomo ships with enable_browser_archiving_triggering = 1. That means opening a dashboard still triggers a live archive run against the raw log tables, archiver or no archiver. On a small site you won’t notice. At a few million hits the dashboard hangs for 30 seconds and your MySQL box pegs a core.

Turn it off after the archiver’s first successful run. In the UI: Admin → System → General settings → Archiving settings → “Archive reports when viewed from the browser” → No. Or in matomo-data/config/config.ini.php:

config.ini.php
[General]
enable_browser_archiving_triggering = 0
archiving_range_force_on_browser_request = 0

Do this after the archiver has run at least once, or your dashboards will be blank and you’ll assume the install is broken.

Setting Up Your Site & Privacy Config

Once Matomo’s running:

  1. Log in with the admin account you created during setup
  2. Go Admin → Websites and add your domain
  3. Copy the tracking code Matomo generates: it’ll look like:
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="http://your-matomo-domain.com/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->

Drop that in your site’s <head> (before closing </head> tag). For Astro, add it to your layout file.

GDPR & Privacy Settings

Go Admin → Privacy → Anonymize data and set:

Then Admin → Privacy → Delete old data and enable deletion of old raw data at 90 days (or 365 if you need year-over-year). Nothing is deleted until you turn this on.

Force HTTPS lives elsewhere. It’s under Admin → System → General settings, not under Privacy. Turn it on once you have a reverse proxy terminating TLS.

Matomo does not ship a consent banner. There is no Admin → Privacy → Consent Manager page that injects one. Matomo’s own developer docs say the product does not yet offer a feature to display a privacy notice. What it gives you is the tracker-side plumbing: call _paq.push(['requireConsent']) before trackPageView, and call _paq.push(['setConsentGiven']) from your own banner’s accept handler. If you want cookie consent specifically rather than tracking consent, the pair is requireCookieConsent and setCookieConsentGiven. Bring your own banner, or a consent management platform, or use Matomo Tag Manager’s consent handling.

Funnels: Actually Tracking Conversions

This is where Matomo stops being “just another analytics tool” and becomes useful.

Go Behavior → Funnels and create one:

Example: E-commerce Funnel

  1. Step 1: Page title contains “Product”
  2. Step 2: Page title contains “Cart”
  3. Step 3: Page URL contains “/checkout”
  4. Step 4: Page URL contains “/thank-you”

Matomo will show you:

Now you’ve got data. “40% of users add items to cart but never checkout”: that’s actionable. You can now fix your checkout flow, test a new payment gateway, reduce friction. Funnels force you to think about the user journey, not just traffic numbers.

Another example: Sign-up Funnel

  1. Visit /signup
  2. Visit /verify-email (or check email)
  3. Visit /dashboard (completed account)

If 30% of signups never verify, your email is going to spam. Fix that before you optimize anything else.

E-commerce Tracking (If You’re Selling Stuff)

These calls go in two different places, and mixing them up is the most common Matomo e-commerce bug.

On a product page, record the view. No cart calls here:

product-page.html
<script>
_paq.push(['setEcommerceView',
"SKU-123", // SKU
"Widget Pro", // product name
"Electronics", // category
29.99 // price
]);
_paq.push(['trackPageView']);
</script>

On the order confirmation page, and only there, add each line item and then close the order:

order-confirmation.html
<script>
_paq.push(['addEcommerceItem',
"SKU-123", // SKU
"Widget Pro", // product name
"Electronics", // category
29.99, // unit price
1 // quantity
]);
_paq.push(['trackEcommerceOrder',
"ORDER-456", // order ID
37.49, // grand total (subtotal + tax + shipping - discount)
29.99, // subtotal
2.50, // tax
5.00, // shipping
0 // discount
]);
_paq.push(['trackPageView']);
</script>

The numbers have to reconcile: trackEcommerceOrder’s first amount is the grand total, not the subtotal. Pass the subtotal in both slots and Matomo will happily report revenue that doesn’t match your payment processor, and you’ll spend an afternoon finding out why. Fire addEcommerceItem once per line item before the order call, and trackEcommerceOrder exactly once per order ID.

Matomo will now track:

You get ROI per traffic source, per page, per user: that’s real business analytics.

Keeping Matomo Fast (And Not Eating Your RAM)

Matomo can get chunky if you’re not careful. Here’s how to keep it lean:

1. Enable the archiver (already in the Compose file above)

Without it, every dashboard view triggers real-time queries against a growing dataset. With it, reports are pre-computed hourly, instant to load.

2. Set automatic data deletion

Go Admin → Privacy → Delete old data. Set it to 90 or 365 days depending on your needs. Old data just sits there eating disk space.

3. Monitor the database

Terminal window
docker compose exec -e MYSQL_PWD="$DB_PASSWORD" db \
mysql -u matomo matomo -e \
"SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.tables
WHERE table_schema = 'matomo'
ORDER BY size_mb DESC LIMIT 10;"

MYSQL_PWD instead of -p<password> keeps the credential out of the container’s process list and your shell history. Load $DB_PASSWORD from the same .env you gave Compose.

If matomo_log_visit is over 5 GB, you’re probably storing too much history. Consider archiving or deleting old data.

4. Add resource limits to Docker

matomo:
# ... other config ...
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
cpus: '1'
memory: 512M

This keeps Matomo from eating your entire server if traffic spikes.

When Matomo Is Actually Worth It

You need Matomo if:

You don’t need Matomo if:

The Setup Checklist

Before you ship this to production:

That’s it. You’re running analytics on your own hardware, with the raw visit log intact, no Google, no surveillance capitalism.

Is it more work than Plausible? Yes, and the plugins you’ll want cost real money. Is it worth it when you need to know which 60% of users bounce off checkout and what those specific sessions did? Yes.

Your 2 AM self, reading an actual visit log instead of guessing at an aggregate, will thank you.

Common Questions

Is Matomo’s funnel feature free on self-hosted?

No. Funnels is a premium Marketplace plugin for Matomo On-Premise. As of September 2026 it’s €199 per year for 1 to 4 users, €386 for 5 to 15, and €579 for unlimited users. The Team bundle at €275 per year includes Funnels plus Heatmaps, Custom Reports, and Form Analytics.

Does Matomo set cookies by default?

Yes. Matomo sets first-party cookies out of the box. Cookieless tracking is opt-in through forceCookielessTracking in config.ini.php or a disableCookies() call in the tracker snippet. IP anonymization is the setting that is on by default, masking the last 2 bytes of every address.

Why are my Matomo dashboards slow even with the archiver running?

Because browser archiving is still on. Matomo defaults enable_browser_archiving_triggering to 1, so every dashboard load re-archives against the raw log tables regardless of your cron. Set it to 0 in config.ini.php after the archiver’s first successful run, then reload.

Can I migrate from Plausible or Umami to Matomo without losing history?

Not directly. Neither Plausible nor Umami exports raw hits in a format Matomo’s importer reads, and Matomo’s own importers target Google Analytics and server log files. Plan to run both tools in parallel for a quarter, then cut over. Your old aggregates stay where they are.

How much RAM does self-hosted Matomo need?

Budget 2 GB for the whole stack on a small site: roughly 512 MB for PHP-FPM or Apache, and the rest for MySQL’s buffer pool. The archiver spikes hardest, so give it headroom rather than a tight limit. Disk matters more than RAM long term, since matomo_log_visit grows with every hit.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Browser Hardening 2026: Brave vs LibreWolf vs Mullvad
Next Post
Falco + Trivy in k3s: Runtime Security on Small Clusters

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts