🎧 Listen to the 60-Second Audio Recap:
Why Self-Host Matomo?
When you self-host Matomo, you get a privacy-friendly alternative to Google Analytics that keeps all data on your own server. Matomo is an open-source web analytics platform. It allows you to keep all your analytics data on your own server, enhancing privacy and ensuring compliance with regulations like GDPR. Following the Schrems II ruling, which raised legal concerns about using Google Analytics in the EU, Matomo offers a solution that emphasizes data ownership and privacy compliance.
The practical difference is simple. With Google Analytics, your visitors’ behaviour is processed on servers you do not control, by a company whose business model is advertising. With a self-hosted Matomo, the raw hits land in a MariaDB database on your own hardware. Nobody samples your data, nobody aggregates it into an advertising profile, and nobody can change the terms of service next quarter.
What You Actually Gain
- Unsampled data. Google Analytics samples reports on high-traffic properties. Matomo reports on every single hit it recorded, no matter how much traffic you send it.
- Raw data access. The visits sit in your own database. You can query them with SQL, export them, or keep them for ten years. Nobody deletes your history for you.
- No cookie banner, if you configure it right. Matomo can run cookieless and with anonymised IPs, which in many jurisdictions removes the legal basis for a consent popup. More on that below.
- No third-party requests. Your visitors’ browsers only talk to your domain. Ad blockers and tracker blockers largely leave first-party Matomo alone, so your numbers are closer to reality than a blocked Google script.
What You Give Up (Read This First)
Honesty matters more than enthusiasm here. Self-hosting analytics means you now own an application, a database, and a maintenance burden. You are responsible for updates, backups, and the archiving cron job that keeps reports fast. If the database fills up your disk at 3 AM, no support team is paged. If that sounds like a bad trade for a hobby blog with twelve visitors a month, it probably is. For anything you care about, it is worth the hour of setup.
Prerequisites & Minimal Hardware
Required Software
- Docker and Docker Compose with Portainer. Refer to our Docker + Portainer on Proxmox LXC guide for setup.
- A domain or subdomain for your Matomo instance (e.g., stats.yourdomain.com) with HTTPS enabled.
- A reverse proxy or Cloudflare Tunnel for managing TLS certificates.
Hardware Requirements
Matomo is a PHP application backed by MariaDB, so it is heavier than a single tracking snippet but still modest by homelab standards. For a small to medium site, plan for:
- CPU: 2 cores. The web interface barely uses them; the archiving cron job is the part that spikes.
- RAM: 2 GB is a comfortable floor for Matomo plus MariaDB. Give it 4 GB if you track several sites, because PHP archiving is memory-hungry.
- Storage: start at 10 GB and put it on SSD or NVMe. The raw visit tables grow with traffic and the archive tables grow with the number of reports and segments you keep. Spinning rust makes archiving painfully slow.
Running this on a mechanical disk is the single most common reason people conclude “Matomo is slow”. It usually is not Matomo.
Method 1: The Quick Start (Beginner Friendly)
Setting Up Matomo Container
We’ll start with a Docker Compose setup that includes Matomo and its required MariaDB database. Matomo has no built-in database, so the database container is mandatory. This first version keeps everything minimal and lets Matomo’s graphical installer do the configuration work.
services:
db:
image: mariadb:11
container_name: matomo_db
restart: unless-stopped
command: --max-allowed-packet=64MB
environment:
MARIADB_ROOT_PASSWORD: ChooseAStrongRootPassword
MARIADB_DATABASE: matomo
MARIADB_USER: matomo
MARIADB_PASSWORD: ChooseAStrongPassword
volumes:
- /mnt/snelle_data/App_Data/matomo_db:/var/lib/mysql
matomo:
image: matomo:5-apache
container_name: matomo
restart: unless-stopped
depends_on:
- db
ports:
- "8080:80"
volumes:
- /mnt/snelle_data/App_Data/matomo:/var/www/html
Two details worth noting. First, there is no version: key at the top. Compose V2 ignores it and warns about it, so modern stacks simply leave it out. Second, the image is pinned to matomo:5-apache rather than latest. Pinning the major version means a docker compose pull gives you security fixes without silently dragging you across a major upgrade while you are not looking. At the time of writing the 5.x branch is at 5.12.
Accessing Matomo
- Open your web browser and navigate to your Matomo instance using your domain (e.g., http://stats.yourdomain.com:8080).
- Complete the graphical web installer. When it asks for database details, enter host
db, usernamematomo, and the password you set in the compose file. - Create your super user account. This account is not the same as a website’s view-only user, so pick a real password and store it in your password manager.
- Add your first website. Matomo asks for the URL and the timezone, and the timezone matters: get it wrong and your daily reports will be cut at the wrong hour forever.
Adding Tracking Code
- Once Matomo is set up, copy the tracking code provided in the dashboard.
- Insert this tracking code into the HTML of the website you wish to track, just before the closing
</head>tag. - Verify the tracking setup by visiting the site and checking the Matomo dashboard for real-time visitor logs.
Method 2: The Pro Setup (Advanced Config/Docker Compose)
The quick start works, but it leaves three things on the table: the database credentials still have to be typed into the installer, nothing restarts the stack in the right order, and reports are archived by whoever happens to open the dashboard. This version fixes all three.
Deploying Multi-Container Stack
services:
db:
image: mariadb:11
container_name: matomo_db
restart: unless-stopped
command: --max-allowed-packet=64MB
environment:
MARIADB_ROOT_PASSWORD: ChooseAStrongRootPassword
MARIADB_DATABASE: matomo
MARIADB_USER: matomo
MARIADB_PASSWORD: ChooseAStrongPassword
volumes:
- /mnt/snelle_data/App_Data/matomo_db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
matomo:
image: matomo:5-apache
container_name: matomo
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
MATOMO_DATABASE_HOST: db
MATOMO_DATABASE_ADAPTER: mysql
MATOMO_DATABASE_USERNAME: matomo
MATOMO_DATABASE_PASSWORD: ChooseAStrongPassword
MATOMO_DATABASE_DBNAME: matomo
PHP_MEMORY_LIMIT: 512M
volumes:
- /mnt/snelle_data/App_Data/matomo:/var/www/html
ports:
- "8080:80"
matomo_cron:
image: matomo:5-apache
container_name: matomo_cron
restart: unless-stopped
depends_on:
- matomo
environment:
PHP_MEMORY_LIMIT: 512M
volumes:
- /mnt/snelle_data/App_Data/matomo:/var/www/html
entrypoint: >
/bin/sh -c "while true; do
php /var/www/html/console core:archive --matomo-domain=https://stats.yourdomain.com;
sleep 3600; done"
The MATOMO_DATABASE_* variables are read by the official image and pre-fill the installer, so you cannot fat-finger the database host. PHP_MEMORY_LIMIT is set on both PHP containers, because archiving runs in the cron container and that is exactly where PHP runs out of memory first.
Configuring Database
- Access the Matomo web installer and confirm the database details, which should already be filled in from the environment variables.
- Create your super-user account and add the first website to track.
Proxy Header Configuration
To track real visitor IPs, configure Matomo to recognize the correct proxy headers. Add the following to config/config.ini.php under [General]:
[General]
assume_secure_protocol = 1
proxy_client_headers[] = "HTTP_X_FORWARDED_FOR"
proxy_host_headers[] = "HTTP_X_FORWARDED_HOST"
Because the Matomo volume is bind-mounted, that file lives at /mnt/snelle_data/App_Data/matomo/config/config.ini.php on the host. You can edit it with your normal editor; no need to shell into the container. Restart the Matomo container afterwards.
Enabling Archive Cron
By default, Matomo processes (“archives”) reports whenever somebody loads the dashboard. On a site with real traffic, that turns a page view into a thirty-second wait. The fix is to archive on a schedule instead, which is what the matomo_cron service above does: it runs the archiver once an hour, forever.
Note the --matomo-domain flag. The older --url option is deprecated since Matomo 5, and guides still using it are out of date.
If you prefer a host cron job over an extra container, this is the equivalent line for the host’s crontab:
5 * * * * docker exec -u www-data matomo php /var/www/html/console core:archive --matomo-domain=https://stats.yourdomain.com
Either way, you must then tell Matomo to stop archiving in the browser:
- Go to Administration > System > General Settings.
- Set “Archive reports when viewed from the browser” to No.
- Set “Archive reports at most every X seconds” to 3600, matching the hourly cron.
If you use custom segments and want to be certain nobody can trigger archiving from the interface, also add browser_archiving_disabled_enforce = 1 to the [General] section of config.ini.php.
Accurate Geolocation Without Selling Your Soul
Out of the box, Matomo guesses a visitor’s country from the browser language, which is close to useless. A visitor with an English-language browser in Rotterdam is filed under the United States. The fix is a local geolocation database, and you do not need a commercial account for it.
- Go to Administration > System > Geolocation.
- Choose DBIP / GeoIP 2 (Php). The PHP implementation needs no extra server modules, which is exactly what you want in a container.
- On the same page, enable the automatic database update. Matomo can pull the free DB-IP City Lite database and refresh it monthly.
DB-IP and MaxMind both offer free databases that are less precise than their paid versions, and MaxMind is generally more accurate at city level. For country-level reporting, which is all most self-hosters need, the free DB-IP database is entirely adequate. The important part is that the lookup happens on your server, against a file on your disk, with no per-visitor API call to a third party. Adding a geolocation service that phones home would defeat the entire purpose of this guide.
Running Matomo Without a Cookie Banner
This is the reason many people self-host Matomo in the first place, so it deserves its own section. Matomo can be configured so that it does not process personal data in the sense that triggers a consent requirement in most European implementations. Two settings do the heavy lifting, both under Administration > Privacy > Anonymize data:
- Anonymize Visitors’ IP addresses. Enable it and choose how many bytes to mask. Matomo masks the address before it is written, so the full IP never reaches your database.
- Deactivate all tracking cookies. This one is hidden behind “Show advanced options” on the same page. With it enabled, Matomo tracks without setting any cookie at all.
Cookieless tracking costs you some accuracy: without a cookie, Matomo has a harder time recognising a returning visitor, so your “returning visitors” metric becomes an estimate. For most self-hosted sites that is a trade worth making. Whether this fully removes your consent obligation depends on your jurisdiction and on what else your site loads, so treat it as a strong technical foundation rather than legal advice.
Configuration & Validation (How to test it)
Tracking Verification
Place the tracking code on a test page, visit it, and check Matomo’s visitor log to see if your visit is logged in real-time. Use Visitors > Visits Log rather than the dashboard summary, because the summary depends on archiving and may lag by up to an hour once you have moved to cron archiving.
Proxy Verification
Ensure that the correct visitor IPs are logged, not the proxy IPs, by checking the visitor’s location in Matomo. If every visitor shares one address, and that address is your reverse proxy or Cloudflare Tunnel, your proxy headers are not being read.
Privacy Verification
Open your browser’s developer tools (Network tab) and ensure there are no requests to google-analytics.com or googletagmanager.com. While you are there, check the Application tab: with cookieless tracking enabled, there should be no _pk_id or _pk_ses cookie set for your domain.
Archiving Verification
Watch the cron container for one cycle with docker logs -f matomo_cron. A healthy run ends with a summary of archived websites and no PHP fatal errors. If you see a memory exhaustion message, raise PHP_MEMORY_LIMIT and restart the container.
Backups and Upgrades
Matomo keeps its state in two places, and a backup that misses either one is not a backup.
- The database, which holds every visit and every archived report.
- The config directory, specifically
config/config.ini.php, which holds your database credentials and every setting you added by hand.
A database dump from the host looks like this:
docker exec matomo_db mariadb-dump -u matomo -p'ChooseAStrongPassword' \
--single-transaction --routines --triggers --hex-blob matomo \
> /mnt/opslag/backups/matomo-$(date +%F).sql
The --single-transaction flag lets the dump run without locking the tables, so tracking continues while you back up. Because the Matomo files are on a bind mount, your existing file-level backup of /mnt/snelle_data/App_Data/matomo already covers the config directory.
For upgrades, take a fresh dump first, then pull the new image and let Matomo migrate its own schema:
docker compose pull
docker compose up -d
docker exec -u www-data matomo php /var/www/html/console core:update
On a large database, that update can take a while. If you want to be careful, set maintenance_mode = 1 in the [General] section of config.ini.php before starting and remove it when the update finishes.
The Ugly Truth (Honesty check / Quirks)
Performance Considerations
Matomo requires more resources than a simple Google Analytics snippet. It’s a full-fledged PHP application with a database that requires maintenance, especially on high-traffic sites. Smaller sites run smoothly, but larger sites may need regular tuning, particularly with the archive cron job.
The Database Grows Forever
Every visit is a row, and archived reports are rows too. Nothing prunes itself unless you tell it to. Under Administration > Privacy > Anonymize data you can enable regular deletion of old raw data while keeping the aggregated reports, which is usually the right compromise: you keep your long-term trend lines and drop the visit-level detail you will never look at again.
Common Pitfalls
Incorrect proxy-header configuration can result in all visitors appearing as your reverse proxy IP. Ensure your configuration is correct to maintain accurate statistics.
Your Numbers Will Not Match Google Analytics
When people run both side by side, Matomo usually reports more traffic. That is not a bug. First-party Matomo is blocked far less often than Google’s script, and Matomo does not sample. Do not waste an afternoon reconciling the two; pick one as your source of truth.
Troubleshooting Common Errors
Trusted Hosts Error
If you encounter a ‘trusted hosts’ error, add your domain under Administration > General Settings > Trusted Hostnames or manually in config/config.ini.php under [General]:
trusted_hosts[] = "yourdomain.com"
Proxy IP Issue
If all visitors appear with the same IP (your proxy’s), ensure the proxy headers are correctly configured as shown earlier.
Slow or Empty Reports
If reports are slow or empty, disable browser-triggered archiving and ensure the cron job for archiving is active.
Redirect Loop Behind a Reverse Proxy
If the login page reloads endlessly over HTTPS, Matomo does not realise the connection is already encrypted. The assume_secure_protocol = 1 line in the proxy configuration above is what fixes this.
Database Connection Refused on First Start
MariaDB initialises its data directory on the very first run, which takes longer than Matomo’s patience. With the healthcheck from Method 2 this resolves itself; without it, simply restart the Matomo container once the database has settled.
Archiving Runs Out of Memory
A PHP “Allowed memory size exhausted” message during archiving means the cron container needs more headroom. Raise PHP_MEMORY_LIMIT to 1G and restart. Sites with many segments or many tracked domains hit this first.
Conclusion & Next Steps
Summary
By following this guide, you have set up a privacy-friendly web analytics platform with Matomo, maintaining full control over your data without relying on Google. You pinned the image to a major version, moved archiving to a cron container so the dashboard stays fast, put geolocation on a local database, and turned off the cookies that would have forced a consent banner.
Next Steps
- Activate IP anonymization and cookieless tracking to potentially eliminate the need for a cookie banner.
- Schedule the database dump above and confirm you can actually restore it. An untested backup is a rumour.
- If you use WordPress, consider installing the official Matomo plugin for integrated analytics within your dashboard.
- Set up email reports under Personal > Email Reports so you get a weekly summary without logging in.
Matomo is one piece of a Google-free stack. To replace more Google services on your own server, see our guides on Immich (a Google Photos alternative) and Nextcloud (a Google Drive alternative), or revisit the Docker + Portainer on Proxmox LXC foundation guide.