Matomo with Docker Compose banner showing an analytics chart, containers and a privacy shield
Self-hosting

Matomo Docker Compose: Self-Hosted Google Analytics Alternative

Google Analytics is free because you pay with something else: your visitors’ data, a cookie banner most people dismiss in annoyance, and reports you only ever see through someone else’s lens. If you run a small business site, a blog or a SaaS landing page, there is a cleaner option. Matomo is an open source web analytics platform that you install on your own server, so every pageview stays on hardware you control, in a data center you can actually name. In this tutorial we deploy it with Docker Compose on a fresh VPS, put it behind automatic HTTPS, add a cookieless tracking snippet to your website and set up the background archiving that keeps reports fast.

Matomo (formerly Piwik) lives at matomo.org , with the source code on github.com/matomo-org/matomo , under the GPLv3 license. The self-hosted edition is free, with optional paid plugins for things like heatmaps or A/B testing. Everything we use below is part of the free core.

What Matomo is and why you would self-host it

Matomo works the same way most analytics tools do. A small JavaScript file (matomo.js) runs in your visitor’s browser and sends a tiny HTTP request to your Matomo server (matomo.php) for each pageview, click on an outgoing link or download. Matomo stores these raw hits in a database. Periodically, a process called archiving crunches the raw hits into reports: visits per day, top pages, referrers, countries, devices and so on. The dashboard reads those pre-computed reports, which is why it can stay fast even with millions of hits.

Why bother hosting it yourself instead of using Google Analytics?

  • You own the data. It never leaves your server, nobody else uses it for advertising, and you decide how long to keep it.
  • No sampling. Every visit counts, reports are not estimated from a subset.
  • GDPR is much simpler. With cookieless tracking and IP anonymization, many sites can run analytics without a consent banner (more on this, and its limits, in step 9).
  • It is fast to load. The tracker is one small script served from your own domain.

A few terms before we start

If some of these are new, here is the short version:

  • Container: an isolated package that contains an application plus everything it needs to run (libraries, runtime). It shares the host’s Linux kernel, so it is much lighter than a virtual machine.
  • Docker Compose: a tool that starts several containers together from one YAML file (compose.yaml), including their networks and storage.
  • Volume: a folder that survives when a container is recreated. Without volumes, your database would disappear on every update.
  • Port: a numbered “door” on a server. Web traffic uses port 80 (HTTP) and 443 (HTTPS), SSH uses port 22.
  • Reverse proxy: a web server that sits in front of your application, terminates HTTPS and forwards requests to the app. We use Caddy, which obtains and renews free Let’s Encrypt certificates automatically.
  • Cron: the Linux scheduler that runs commands at fixed times. We use it to run Matomo’s archiving every hour.

How the pieces fit together

Our stack has three containers: Caddy (HTTPS), Matomo (the Apache + PHP application) and MariaDB (the database). Only Caddy publishes ports to the internet. Matomo and MariaDB talk to each other over a private Docker network and are not reachable from outside.

InternetYou (admin)SSH clientVisitor browserruns matomo.jsYour websiteany host or CMSpage + snippetDreamServer VPS (Debian 13 / Ubuntu 24.04)ufwfirewallsshd :22admin SSH22cron, hourlycore:archiveDocker networkdocker compose execCaddyTLS, ports 80/443MatomoApache + PHPcaddy_datacertificatesMariaDB 11.8no public port./matomocode + configdb_datadatabase files443Note: Docker-published ports (80/443) bypass ufw. ufw only guards host services like SSH.MariaDB and Matomo publish no ports, they are reachable only inside the Docker network.
The Matomo Docker Compose stack: browsers talk HTTPS to Caddy, Caddy forwards to Matomo, Matomo stores data in MariaDB, and a host cron job triggers archiving every hour.

One detail in that diagram matters a lot for security: Docker writes its own firewall rules and bypasses ufw for any port it publishes. That is why we never publish the database or Matomo’s internal port, and why ufw is drawn only in front of SSH. If a container does not have a ports: section, nothing on the internet can reach it, regardless of ufw.

What you need

  • A DreamServer VPS running Debian 13 or Ubuntu 24.04, ideally KVM, with at least 2 GB RAM, 2 vCPU and 20 GB of disk.
  • A sudo user and SSH access to the server.
  • A domain or subdomain you control, for example analytics.example.com. We use that name in all examples, replace it with yours everywhere.
  • Ports 80 and 443 free on the VPS (no other web server such as Apache or nginx installed directly on the host).
  • About 45 minutes.

A note for LXC users: Docker inside an unprivileged LXC container only works if the container has the nesting feature enabled, and ufw often cannot load its kernel modules inside LXC at all. If you are on an LXC VPS and something in steps 1 or 2 fails, ask our support to enable nesting, or use a KVM VPS, where everything below works as written.

How big a VPS do you need?

Matomo itself is modest. The heavy moment is archiving, when PHP reads raw hits from MariaDB and builds reports. For a small site with a few thousand pageviews per day, the numbers look roughly like this:

Approximate RAM use of the Matomo stack (small site)~25 MB~90 MB~180 MB~300 MB~250 MB extraCaddyMatomo (idle)MariaDBStack total (idle)Archiving peak0100200300400Memory in MB (approximate values)Container / process
Typical, approximate memory use for a site with a few thousand pageviews per day. A 2 GB VPS leaves comfortable headroom for archiving and the operating system.

As a rule of thumb, Matomo’s own guidance puts sites under about 100,000 pageviews per month on a single server with 2 CPU and 2 GB of RAM. Much bigger traffic mostly needs more RAM for MariaDB and more disk for raw data, and both are easy to scale up later.

The procedure at a glance

Here is the whole job in nine steps. Each numbered heading below matches one box in this diagram.

Matomo with Docker Compose: the 9 steps1DNS + firewall2Install Docker3Folder + secrets4Compose + Caddy5Start + installer6Trust the proxy7Tracking code8Cron archiving9Privacy settingsPrepare the VPSDeploy the stackConfigure Matomo
The nine steps of this tutorial: steps 1-2 prepare the VPS, steps 3-5 deploy the stack, steps 6-9 configure Matomo.

Step 1: Point DNS and open the firewall

Caddy can only get a certificate if your analytics subdomain already points to the VPS. In your DNS provider’s panel, create an A record for analytics.example.com with your VPS’s IPv4 address. If your VPS has IPv6, add an AAAA record too, but only with the correct address: a wrong AAAA record is the most common reason Let’s Encrypt validation fails.

Check that the name resolves (it can take a few minutes after you create the record):

getent hosts analytics.example.com

You should see your VPS IP address next to the name. If you see nothing, wait a bit and try again.

Now the host firewall. We allow SSH first, so enabling the firewall cannot lock you out. If your SSH runs on a different port, use that number instead of 22.

sudo apt update
sudo apt install -y ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw enable
sudo ufw status verbose

ufw allow 443 without a protocol opens both TCP and UDP, the UDP part is for HTTP/3. The last command should list your three rules with ALLOW IN. Strictly speaking, the 80 and 443 rules are not what lets traffic reach Caddy (Docker handles that, as explained above), but they document your intent and keep things consistent if you ever run a web server directly on the host.

Step 2: Install Docker Engine and the Compose plugin

Start with a fully updated system:

sudo apt update && sudo apt full-upgrade -y

If a new kernel was installed, reboot before continuing. On Ubuntu, the file /var/run/reboot-required appears when a reboot is needed (ls /var/run/reboot-required). On Debian, that file is not always created, so simply reboot if the upgrade installed a new linux-image package.

We install Docker from Docker’s own repository rather than the distribution packages, because it ships the current Engine and the docker compose plugin. This block works on both Debian and Ubuntu: it reads the distribution name and codename from /etc/os-release, then writes the repository in Docker’s current deb822 format.

sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/${ID}
Suites: ${VERSION_CODENAME}
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify the installation:

sudo docker run --rm hello-world
sudo docker compose version

The first command prints a “Hello from Docker!” message, the second prints the Compose version (v2.x or newer). In this guide we prefix Docker commands with sudo. You could add your user to the docker group instead, but be aware that membership in that group is effectively root access.

Step 3: Create the project folder and secrets

Everything for this stack lives in /opt/matomo: the Compose file, the Caddy config, a secrets file and Matomo’s own files.

sudo mkdir -p /opt/matomo
cd /opt/matomo
sudo sh -c 'umask 077; cat > /opt/matomo/.env <<EOF
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 24)
MATOMO_DB_PASSWORD=$(openssl rand -hex 24)
EOF'
sudo ls -l /opt/matomo/.env

This generates two random 48-character passwords, one for MariaDB’s root account and one for the matomo database user, and stores them in .env. Docker Compose reads this file automatically and substitutes the values into compose.yaml. We use hexadecimal passwords on purpose: no quotes, dollar signs or other characters that could break YAML or Matomo’s config file. The umask 077 makes the file readable only by root, and ls -l should show -rw-------.

Important: MariaDB reads these passwords only the first time it starts with an empty data volume. If you change .env later, the database password does not change. Keep this file as it is, and back it up.

Step 4: Write compose.yaml and the Caddyfile

Create the Compose file:

sudo nano /opt/matomo/compose.yaml

Paste this content:

services:
  db:
    image: mariadb:11.8
    restart: unless-stopped
    command: --max-allowed-packet=64MB
    environment:
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MARIADB_DATABASE: matomo
      MARIADB_USER: matomo
      MARIADB_PASSWORD: ${MATOMO_DB_PASSWORD}
      MARIADB_AUTO_UPGRADE: "1"
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  matomo:
    image: matomo:5-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      MATOMO_DATABASE_HOST: db
      MATOMO_DATABASE_ADAPTER: mysql
      MATOMO_DATABASE_TABLES_PREFIX: matomo_
      MATOMO_DATABASE_USERNAME: matomo
      MATOMO_DATABASE_PASSWORD: ${MATOMO_DB_PASSWORD}
      MATOMO_DATABASE_DBNAME: matomo
    volumes:
      - ./matomo:/var/www/html

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    depends_on:
      - matomo
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  db_data:
  caddy_data:
  caddy_config:

What each part does:

  • db runs MariaDB. The image creates the matomo database and user on first start. --max-allowed-packet=64MB is the value Matomo recommends for bulk inserts. MARIADB_AUTO_UPGRADE upgrades the system tables automatically when you pull a newer 11.8 patch release. The healthcheck lets Compose wait until the database really accepts connections.
  • matomo runs the official Matomo image with Apache and PHP. The MATOMO_DATABASE_* variables pre-fill the database screen of the web installer. The bind mount ./matomo keeps Matomo’s code, configuration and plugins in /opt/matomo/matomo, where you can easily edit and back them up. It has no ports: section, so only Caddy can reach it.
  • caddy is the only container with published ports. Its volumes keep the TLS certificates across restarts.

About image tags: matomo:5-apache follows the current Matomo 5 major line, mariadb:11.8 is the current long-term support series and caddy:2-alpine the current Caddy major. We avoid :latest, which could jump to a new major version unexpectedly. Before deploying, check the Matomo changelog , the MariaDB release notes and the image tag pages for newer versions.

Now the Caddy configuration:

sudo nano /opt/matomo/Caddyfile
{
	email you@your-domain.com
}

analytics.example.com {
	encode zstd gzip

	@private path /config/* /tmp/* /core/* /lang/*
	respond @private 403

	reverse_proxy matomo:80
}

Replace the email (Let’s Encrypt uses it for expiry warnings, placeholder domains like example.com are rejected) and the domain. The @private block refuses direct access to Matomo’s internal folders, matching the rules Matomo recommends for its own nginx setups. reverse_proxy matomo:80 forwards everything else to the Matomo container, using its service name as the hostname. Caddy also adds the X-Forwarded-For and X-Forwarded-Proto headers that we will tell Matomo to trust in step 6.

Check the Compose file for typos:

cd /opt/matomo
sudo docker compose config --quiet

No output means the file is valid.

Step 5: Start the stack and run the Matomo installer

cd /opt/matomo
sudo docker compose up -d
sudo docker compose ps

The first start downloads the images, so give it a minute. docker compose ps should list three services with status Up, and the db service should show (healthy). Now check that Caddy obtained a certificate:

sudo docker compose logs caddy | grep -i "certificate obtained"

You should see a line containing certificate obtained successfully. If not, jump to the troubleshooting section.

Open https://analytics.example.com in your browser right away. Until you finish the installer, anyone who finds the URL could complete it and become the administrator, so do not leave it half-done. The installer walks you through these screens:

  1. Welcome, then System Check: everything important should be green. Warnings about optional features are fine.
  2. Database Setup: thanks to our environment variables, the fields are pre-filled (server db, login matomo, database matomo, prefix matomo_, adapter PDO\MYSQL). Just confirm.
  3. Creating the tables: takes a few seconds.
  4. Superuser: choose a username, a strong password and your email. This is your admin account.
  5. Set up a website: enter the name and URL of the first site you want to track, plus its time zone.
  6. Tracking code: shows the JavaScript snippet. You can skip it for now, we will add an improved version in step 7.
  7. Congratulations: decide whether to enable Do Not Track support, then continue to the dashboard.

Step 6: Make Matomo trust the reverse proxy

From Matomo’s point of view, every request now comes from Caddy, over plain HTTP, from an internal Docker IP like 172.18.0.4. Without the next change, all visitors would appear to have the same IP address and Matomo could generate http:// links. We fix that in Matomo’s config file, which lives in our bind mount:

sudo nano /opt/matomo/matomo/config/config.ini.php

Find the existing [General] section (do not create a second one) and add these lines under it:

assume_secure_protocol = 1
force_ssl = 1
proxy_client_headers[] = HTTP_X_FORWARDED_FOR
proxy_host_headers[] = HTTP_X_FORWARDED_HOST
  • assume_secure_protocol tells Matomo that users really see HTTPS, even though Caddy talks to it over HTTP.
  • force_ssl makes Matomo always build HTTPS URLs.
  • proxy_client_headers[] makes Matomo read the real visitor IP from the header Caddy sets.
  • proxy_host_headers[] does the same for the hostname.

If the installer already wrote some of these lines, do not duplicate them. Trusting X-Forwarded-For is safe here because Matomo is reachable only through Caddy, and Caddy replaces any forged value sent by a client. Save, then restart the container to be sure the change is picked up:

cd /opt/matomo
sudo docker compose restart matomo

Step 7: Add the cookieless tracking code to your site

In Matomo, open Administration (the gear icon) > Websites > Manage to add more sites; each one gets a numeric site ID. The first site you created in the installer has ID 1. Here is the standard Matomo snippet with one extra line, disableCookies, which makes tracking cookieless:

<!-- Matomo -->
<script>
  var _paq = window._paq = window._paq || [];
  _paq.push(['disableCookies']);
  _paq.push(['trackPageView']);
  _paq.push(['enableLinkTracking']);
  (function() {
    var u="https://analytics.example.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 -->

Paste it just before the closing </head> tag of every page, or in your CMS’s “header scripts” setting or theme template. Change the URL and the site ID to match yours. The script loads asynchronously, so it does not slow your page down.

What disableCookies changes: Matomo stops setting its _pk_id and _pk_ses cookies. Visits are still counted, and within a single visit, page views are still grouped together using a short-lived hash computed on the server. The trade-off is that a visitor returning on another day is usually counted as a new visitor, so “returning visitors” and “unique visitors” become approximate. For most small sites that is a very good deal in exchange for not storing anything in the visitor’s browser.

If your site is a single-page application (React, Vue and similar), the snippet only tracks the first load. On each route change, push ['setCustomUrl', newUrl] and then ['trackPageView'] again.

To verify: open your site in a private window, then in Matomo go to Visitors > Real-time. Your visit should appear within seconds, with your real public IP address, not a 172.x.x.x Docker address. If you use an ad blocker, disable it for this test, some block lists include matomo.js.

Step 8: Set up cron-based archiving

By default, Matomo builds reports when someone opens the dashboard (“browser-triggered archiving”). On a site with real traffic, that makes the dashboard slow, and it can time out on longer date ranges. The fix is to build reports in the background every hour and let the dashboard just read them.

First, make sure cron is installed and running. Ubuntu Server has it by default, while minimal Debian images sometimes do not. The service is called cron on both:

sudo apt install -y cron
sudo systemctl enable --now cron

Run the archiver once by hand to check that it works:

cd /opt/matomo
sudo docker compose exec -T --user www-data matomo php /var/www/html/console core:archive --url=https://analytics.example.com/

--user www-data runs the command as the same user as the web server, so cache files keep the right owner. -T disables the interactive terminal, which cron does not have. You should see log lines for each site and period, ending with a summary and a message that archiving is done.

Now schedule it every hour, at minute 5:

sudo tee /etc/cron.d/matomo-archive > /dev/null <<'EOF'
# Matomo: build reports every hour, at minute 5
5 * * * * root /usr/bin/docker compose -f /opt/matomo/compose.yaml exec -T --user www-data matomo php /var/www/html/console core:archive --url=https://analytics.example.com/ > /var/log/matomo-archive.log 2>&1
EOF

Files in /etc/cron.d include a user column (here root, needed to run Docker). The output of the last run is written to /var/log/matomo-archive.log, overwritten each time, so it never grows. After the next full hour, check it with sudo cat /var/log/matomo-archive.log.

Finally, turn off browser-triggered archiving in Matomo: Administration > System > General settings, under the archiving settings set Archive reports when viewed from the browser to No, and Archive reports at most every X seconds to 3600. Save. From now on, dashboards load almost instantly, and today’s numbers update once an hour (the Real-time and Visits Log views are always live).

Step 9: Tighten the privacy settings for GDPR

Cookieless tracking is half of the privacy story. The other half lives under Administration > Privacy:

  • Anonymize data: set Anonymize Visitors’ IP addresses to Yes, mask 2 bytes (for example 192.168.xxx.xxx), and set Also use the anonymized IP addresses when enriching visits to Yes. Matomo then never stores the full IP, and country detection uses the masked one.
  • Replace User ID with a pseudonym if you ever use the User ID feature.
  • Regularly delete old raw data: enable it and choose a retention period, for example 180 days. This deletes the detailed visit logs while the aggregated reports stay, as long as archiving (step 8) has processed that data first, which it does every hour.
  • Users opt-out: Matomo generates an opt-out snippet you can embed in your privacy policy page, so visitors can refuse tracking with one click.
  • GDPR Tools: lets you find and export or delete all data about a specific visitor, if someone exercises their rights.

Depending on your Matomo version, the anonymization page may also offer a switch that forces tracking without cookies for every site. If you see it, enable it as a safety net, in case someone uses the default snippet without disableCookies.

On the legal side: in several EU countries, regulators accept that audience measurement without cookies, with anonymized IPs, used only for your own statistics and not shared with third parties, can run without consent. Rules differ between countries and this is not legal advice, so mention Matomo in your privacy policy and check with whoever handles your compliance.

Optionally, under Administration > System > Geolocation, you can install a free DB-IP database for more accurate country and city detection than the default language-based guess.

Keeping Matomo up to date

There are two layers to update. The images (Caddy, MariaDB, PHP and Apache inside the Matomo image) update with:

cd /opt/matomo
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -f

The Matomo application itself is different. In the official image, Matomo’s PHP files are copied into ./matomo the first time the container starts. After that they live in your folder, so pulling a new image does not change them. When a new Matomo release is out, the admin interface shows an update notice: take a backup (next section), then use the one-click updater. If the database upgrade is large, finish it from the command line instead of the browser:

cd /opt/matomo
sudo docker compose exec --user www-data matomo php /var/www/html/console core:update --yes

Backing up and restoring Matomo

Two things matter: the database and the /opt/matomo folder (secrets, Compose file, Caddyfile and Matomo’s config). This dumps the database through the container, passing the password via the MYSQL_PWD variable instead of the command line, and compresses it into your home folder:

cd /opt/matomo
sudo docker compose exec -T db sh -c 'MYSQL_PWD="$MARIADB_PASSWORD" mariadb-dump --single-transaction --quick -u matomo matomo' | gzip > ~/matomo-db-$(date +%F).sql.gz
sudo tar czf ~/matomo-files-$(date +%F).tar.gz -C /opt/matomo .env compose.yaml Caddyfile matomo
sudo chown "$USER": ~/matomo-files-$(date +%F).tar.gz

The chown matters: an archive created with sudo tar belongs to root, and you would not be able to download it with scp as your normal user. Copy both files off the server, for example to your laptop or to backup storage. A backup that lives only on the same VPS is not a backup.

To restore, always import into an empty database, never on top of the live one. Stop Matomo first so no tracking requests land in a half-restored database, and comment out the cron line while you work:

cd /opt/matomo
sudo docker compose stop matomo
sudo docker compose exec -T db sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -u root -e "DROP DATABASE IF EXISTS matomo; CREATE DATABASE matomo;"'
gunzip -c ~/matomo-db-2026-11-10.sql.gz | sudo docker compose exec -T db sh -c 'MYSQL_PWD="$MARIADB_PASSWORD" mariadb -u matomo matomo'
sudo docker compose start matomo

Replace the file name with your backup’s date. The matomo user keeps its privileges on the recreated database. When moving to a new server, extract the files archive into /opt/matomo, start only the db service (it creates a fresh, empty database from your .env), import the dump, then start everything.

Troubleshooting: common mistakes

Caddy does not get a certificate. Check that getent hosts analytics.example.com returns your VPS IP, that there is no wrong AAAA record, and that nothing else uses ports 80 or 443: sudo ss -tlnp | grep -E ':(80|443) '. If Apache or nginx is installed on the host, stop and disable it. Then run sudo docker compose restart caddy and read sudo docker compose logs caddy.

The installer cannot connect to the database. Run sudo docker compose ps and wait until db shows (healthy). If you edited .env after the first start, the passwords no longer match: MariaDB only reads them on its very first start. On a brand-new install with no data, the simplest fix is sudo docker compose down -v (this deletes the database volume) followed by sudo docker compose up -d.

All visits show the same 172.x.x.x IP. The proxy settings from step 6 are missing or sit in the wrong section. They must be under [General] in config.ini.php.

Redirect loop or “too many redirects”. You set force_ssl = 1 without assume_secure_protocol = 1. Add both.

“You are accessing Matomo from an untrusted host” warning. You opened Matomo under a different name than the one used during installation. Add the name as an extra trusted_hosts[] = "name" line in [General].

Reports are empty, but Real-time shows visits. Archiving is not running. Read /var/log/matomo-archive.log, check systemctl status cron, and run the manual command from step 8.

Permission errors under tmp/. Someone ran the console as root. Fix ownership with sudo docker compose exec matomo chown -R www-data:www-data /var/www/html/tmp and always use --user www-data.

Fewer visits than expected. Some visitors use ad blockers that block Matomo’s script. That is normal for any analytics tool, do not chase 100% accuracy.

Security notes

  • Finish the installer immediately after the first start. An unfinished installer can be claimed by anyone.
  • Enable two-factor authentication for your admin account: click your username > Security > Two-factor authentication. Use strong passwords for every Matomo user and give non-admins “view” access only.
  • Publish only Caddy’s ports. MariaDB and Matomo have no ports: section, and because Docker-published ports bypass ufw, keep it that way.
  • Protect .env (mode 600) and your backups: they contain database passwords, and the database contains your visitor data.
  • Keep everything updated: the host with apt, the images with docker compose pull, and Matomo with its updater. Consider unattended-upgrades for host security patches.
  • Harden SSH: key-based login, no root password login. ufw protects SSH, not the Docker ports.
  • If only a few people use the dashboard, you can restrict the admin interface to known IPs in the Caddyfile, but keep matomo.js and matomo.php public, otherwise tracking stops.

So, is self-hosted Matomo worth it?

If you want analytics that respect your visitors, keep data in your own infrastructure and do not depend on a third party’s terms, Matomo with Docker Compose is one of the most practical setups you can run: three containers, automatic HTTPS, cookieless tracking and hourly archiving that keeps reports fast. Once installed, it mostly needs an occasional update and a regular backup.

You can run this whole stack on a VPS in our Romanian data center, with your data staying in the EU. If you would rather not deal with updates, backups and tuning yourself, our server administration team can install, secure and maintain Matomo for you.

FAQ

Is Matomo really free?

Yes. The self-hosted Matomo On-Premise edition is free and open source under GPLv3, with no limits on sites or pageviews. Some advanced features, such as heatmaps, session recordings or A/B testing, are paid plugins, but everything in this tutorial is included in the free core.

Often not for analytics alone, but it depends on your country and on what else your site loads. Cookieless tracking with anonymized IPs, used only for your own statistics, is accepted without consent by several EU regulators. Other tools on your site (embedded videos, ad scripts, chat widgets) may still require consent. Check with your legal advisor.

Can I import my old Google Analytics data?

Yes. Matomo offers a free Google Analytics Importer plugin from its marketplace, which pulls historical reports from your GA property into Matomo. On large properties the import can take a long time, so start it early.

Can one Matomo instance track several websites?

Yes. Add each site under Administration > Websites > Manage. Each gets its own site ID, which you put in that site’s tracking snippet. One small VPS can comfortably handle many low-traffic sites.

Why MariaDB and not SQLite or PostgreSQL?

Matomo officially supports MySQL and MariaDB only. MariaDB is fully open source, well tested with Matomo and has an official Docker image with a built-in healthcheck, which makes it the natural choice for this stack.

Trusted By & Member Of

We are proud members of leading internet infrastructure organizations.

RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX