Visitor statistics: Matomo, Google Analytic replacement self-hosting


For those who already use Docker, the open source web analytics software Matomo to easily collect and analyze visitor statistics for their own websites. Unlike Google Analytics, this does not allow the data to be sent to a cloud service, but rather collected on your own self-hosted server.


current version 4.16.0

Docker Basics

Docker allows applications to be launched by command in a so-called container.
A container is an isolated environment independent of the operating system (OS):
When a container is first launched, Docker independently loads all the necessary sources
from the internet.
Docker can be installed on Windows, macOS or an Linux Distribution

To ensure that Matomo is can be reached securely from the Internet, I use a Let's Encrypt reverse proxy. At first I used Nginx as Reverse-Proxy, but later replaced it with Traefik. The reverse proxy provides an encrypted HTTPS connection and makes it possible to run multiple websites on one server.

Step by step Matomo and Docker including access from the internet

Hardware requirement:
  1. Almost any hardware can be used for the Docker installation. For example, a virtual server of a provider, or for home: a Mini-PC, notebook, MAC, a NAS: QNAP, Synology or any other hardware with x68-64 architecture on which Windows or Linux can be installed.

Schematic representation: Access from the Internet
Internet access requirements:
  1. Own registered domain, see domain and its management.
  2. Cloudflare or Reverse Proxy:
    1. Cloudflare Tunnel Service
    2. or alternatively:
    3. Port-Forwarding and Reverse Proxy mit Let's Encrypt-Zertifikat
    see also: Cloudflare or Reverse Proxy
The actual Docker setup for Matomo

  1. create docker-compose.yml, matomo.conf, db.env and customize it
  2. Start the containers and
  3. Setup

Matomo Docker Compose

To launch Matomo using docker compose, the official Docker Matomo image can be used and MariaDB can be used as the database. Both images can be downloaded, created and launched with a simple docker-compose.yml file. The file can be filled with any text editor as follows and then customized:

Filename: docker-compose.yml, Content:

    image: mariadb    
    container_name: matomo-db
    command: --max-allowed-packet=64MB
    restart: always
      - ./db:/var/lib/mysql
      - ./db.env
    image: matomo:fpm-alpine
    restart: always
    container_name: matomo-app
      - db
      - ./data:/var/www/html
      - PHP_MEMORY_LIMIT=2048M
      - ./db.env
    image: nginx:alpine
    container_name: matomo-web
    restart: always
      - ./data:/var/www/html:ro
      # see
      - ./matomo.conf:/etc/nginx/conf.d/default.conf:ro
      - "80"   
   #For direct test access, remove "#" in the following 2 lines. Call: http://localhost:80 or http://ServerIP:80
      #- "80:80" 
    #Labels for ReverseProxy, see: 
      - "traefik.enable=true"
      - "traefik.http.routers.matomo.rule=Host(`analytics.domain.tld`)"   
      - "traefik.http.routers.matomo.entrypoints=web" 
      - "traefik.http.routers.matomo.entrypoints=websecure"
      - "traefik.http.routers.matomo.tls.certresolver=myresolver"  
#Without using a reverse proxy ( the webproxy network is likely to be missing
#and the following lines can be removed or commented out. Alternatively, the network can be created with "docker network create webproxy".
    name: webproxy
    external: true

For direct access via IP address or localhost - also without reverse proxy, DNS or public IP - the commented out port setting can be activated for test purposes by adding "#" in front of "ports" and "-"80:80"" is removed. For Internet access via the Traefik reverse proxy, the domain must be replaced in the labels with the previously created DNS records (in the example: uptime.domain.tld). The example uses Bind mounts and not volumes for permanently storing the data. See: Docker data storage: Docker volumes vs. host folders.

Additionally, I created the following configuration file in the folder:


upstream php-handler {
	server app:9000;
    gzip  on;
    gzip_vary on;
    gzip_min_length 10240;    
    gzip_proxied any;
    gzip_disable msie6;
    gzip_comp_level 1;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

server {
	listen 80;

	add_header Referrer-Policy origin; # make sure outgoing links don't show the URL to the Matomo instance
	root /var/www/html; # replace with path to your matomo instance
	index index.php;
	try_files $uri $uri/ =404;

	## only allow accessing the following php files
	location ~ ^/(index|matomo|piwik|js/index|plugins/HeatmapSessionRecording/configs).php {
		# regex to split $uri to $fastcgi_script_name and $fastcgi_path
		fastcgi_split_path_info ^(.+\.php)(/.+)$;

		# Check that the PHP script exists before passing it
		try_files $fastcgi_script_name =404;

		include fastcgi_params;
		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
		fastcgi_param PATH_INFO $fastcgi_path_info;
		fastcgi_param HTTP_PROXY ""; # prohibit httpoxy:
		fastcgi_pass php-handler;

	## deny access to all other .php files
	location ~* ^.+\.php$ {
		deny all;
		return 403;

	## disable all access to the following directories
	location ~ /(config|tmp|core|lang) {
		deny all;
		return 403; # replace with 404 to not show these directories exist
	location ~ /\.ht {
		deny all;
		return 403;

	location ~ js/container_.*_preview\.js$ {
		expires off;
		add_header Cache-Control 'private, no-cache, no-store';

	location ~ \.(gif|ico|jpg|png|svg|js|css|htm|html|mp3|mp4|wav|ogg|avi|ttf|eot|woff|woff2|json)$ {
		allow all;
		## Cache images,CSS,JS and webfonts for an hour
		## Increasing the duration may improve the load-time, but may cause old files to show after an Matomo upgrade
		expires 1h;
		add_header Pragma public;
		add_header Cache-Control "public";

	location ~ /(libs|vendor|plugins|misc/user) {
		deny all;
		return 403;

	## properly display textfiles in root directory
	location ~/(.*\.md|LEGALNOTICE|LICENSE) {
		default_type text/plain;

# vim: filetype=nginx

And the variables in the db.env file.


Once the 3 files have been created and customized, the container can be started:

Start container

The start is done from the folder of the docker-compose.yml file with the command "docker-compose up":

docker-compose up -d

Startup / first call of the web interface

After startup, Matomo should be accessible from the specified domain. The first call leads through the setup wizard:

Once Matomo is set up, websites can be created. In order for the counter to start tracking, a tracking code must of course be stored on the website itself: here are the possible options:


In order to not affect the loading of the website, I placed the tracking code at the end of the website inside the body element:

The tracking code can be found under Websites / Tracking Code:

Here is an example:

<!-- Matomo -->
var _paq = window._paq = window._paq || [];
  /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
  (function() {
    var u="//analytics.domain.tld/";
    _paq.push(['setTrackerUrl', u+'matomo.php']);
    _paq.push(['setSiteId', '5']);
    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);
<!-- End Matomo Code -->

For WordPress, there is also a plugin for Matomo, see:

For those who want to host WordPress themselves, see: WordPress in Docker incl. HTTPS Let's Encrypt setup.

positive Bewertung({{pro_count}})
Rate Post:
{{percentage}} % positive
negative Bewertung({{con_count}})

THANK YOU for your review!

Updated: 2023-11-29 von Bernhard | Übersetzung Deutsch |🔔 | Comments:0

Home Assistant: Docker startup + SSL Internet access | Container | Running Docker Mailserver yourself | a field report
Webhosting | Missing wp-admin? Why this site doesn't use WordPress.

Top articles in this section

Commissioning Zigbee2MQTT in Docker - step by step

Zigbee2MQTT is an open source Zigbee bridge which can be easily integrated into existing smart home solutions thanks to the MQTT network protocol. As an example, Zigbee2MQTT combined with MQTT broker Mosquitto and Home Assistant can collect, display, record and control data from Zigbee devices. The setup described here uses Docker as a base. Manufacturer's website:

Send signal messages via script / API: signal-cli-rest-api

The well-known Signal Messenger can besides the app also be used via command line or from other systems. The signal-cli-rest-api, which is available in the form of a Docker container, offers a simple option for this. For the API to work, it has to be coupled via the Signal app beforehand.

Nextcloud Server Docker | Setup + https: Let's Encrypt [ssl]

To synchronize contacts, appointments, and photos of my NAS, I tested Nextcloud and thus turned my back on other cloud providers for my private data. Thanks to Docker, the installation is easier and more flexible than ever, allowing Nextcloud to run on almost any hardware.

Questions / Comments

By continuing to browse the site, you agree to our use of cookies. More Details