Matomo (formerly known as Piwik) is a simple yet advanced tool for seeing where your website visitors are coming from, what pages are being viewed, what links are clicked, and various other points that are useful.

All while securing the privacy of your viewers, such as removing the last part of the IP Address.

Here we will be showing how to set it up in containers with with Docker Compose.

Setup

We will put all the data and configuration in one folder, for this demonstration. Here’s a idea of what the layout will be:

  • env will have the timezone in it.
  • env-db will have the database username and password, for the initial setup.
  • db directory will hold the database files.
  • config directory will hold Matomo’s configuration files.
  • default.conf will be our NGINX configuration, as provided below.

Environment

Create a file for you environment called env that has your time zone:

TZ=America/Denver

Then you need to make another one called env-db that has the MariaDB variables. Fill in all the RandomChars and RandomPassword strings below with whatever you want. MariaDB will create the database, usernames, and passwords as needed. If you are migrating the site, set these to what your current site’s database authorization is in Matomo.

MYSQL_ROOT_PASSWORD=yourRandomPassword
MYSQL_DATABASE=matomo_RandomChars
MYSQL_USER=matomo_RandomChars
MYSQL_PASSWORD=yourOtherRandomPassword

NGINX

The setup will use nginx to relay traffic to the site. We will use this configuration file. Save it to ‘default.conf’.

upstream php-handler {
    server app:9000;
}

server {
    listen 80;
    server_name _;
    # send everything to output, as we don't keep logs in the container
    access_log /dev/stdout;
    error_log /dev/stdout warn;

    root /var/www/html/;

    # index index.php;
    index index.php index.html index.htm;

    ## only allow accessing the following php files
    location ~ ^/(index|matomo|piwik|js/index).php {
        if (!-f $document_root$fastcgi_script_name) { return 404; }
        fastcgi_param HTTP_PROXY        ""; # prohibit httpoxy: https://httpoxy.org/
        fastcgi_split_path_info         ^(.+\.php)(/.+)$;
        fastcgi_param SCRIPT_FILENAME   $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO         $fastcgi_path_info;
        fastcgi_index                   index.php;
        fastcgi_pass                    php-handler;
        include                         fastcgi_params;
    }

    ## needed for HeatmapSessionRecording plugin
    location = /plugins/HeatmapSessionRecording/configs.php {
        if (!-f $document_root$fastcgi_script_name) { return 404; }
        fastcgi_param HTTP_PROXY        ""; # prohibit httpoxy: https://httpoxy.org/
        fastcgi_split_path_info         ^(.+\.php)(/.+)$;
        fastcgi_param SCRIPT_FILENAME   $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO         $fastcgi_path_info;
        fastcgi_index                   index.php;
        fastcgi_pass                    php-handler;
        include                         fastcgi_params;
    }

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

    ## serve all other files normally
    location / {
        try_files $uri $uri/ =404;
    }

    ## disable all access to the following directories
    location ~ /(config|tmp|core|lang) {
        deny all;
        return 403;
    }
    location ~ /\.ht {
        deny  all;
        return 403;
    }

    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) {
        return 404;
    }
}
# vim: filetype=nginx

Docker Compose

Here is the docker-compose.yml configuration. Note that I have added the traefik configuration to it, though you can remove that if needed (just remove the traefik_proxy and labels entries.)

You’ll also see that we don’t use links because each container can reach the other by it’s name. ie, app reaches db by simply calling out for db. See the above fcgi for how it’s calling app in the upstream php-handler.

version: '2.2'
services:

  db:
    image: mariadb:latest
    restart: always
    networks:
      - internal
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./db:/var/lib/mysql
    env_file:
      - ./env
      - ./env-db
    command: --max_allowed_packet=64M

  app:
    image: matomo:fpm
    hostname: YOUR.SITE.EXAMPLE.COM
    restart: always
    environment:
      - TZ=America/Denver
    networks:
      - internal
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config:/var/www/html/config:rw
      - ./logs:/var/www/html/logs
    depends_on:
      - db

  web:
    image: nginx:latest
    restart: always
    volumes:
      - ./default.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - internal
      - traefik_proxy
    volumes_from:
      - app
    environment:
      - TZ=America/Denver
    labels:
      - "traefik.enable=true"
      - "traefik.backend=matomo"
      - "traefik.frontend.rule=Host:YOUR.SITE.EXAMPLE.COM"
      - "traefik.port=80"
      - "traefik.docker.network=traefik_proxy"
      - "traefik.frontend.headers.SSLRedirect=true"
      - "traefik.frontend.headers.STSSeconds=315360000"
      - "traefik.frontend.headers.browserXSSFilter=true"
      - "traefik.frontend.headers.contentTypeNosniff=true"
      - "traefik.frontend.headers.forceSTSHeader=true"
      - "traefik.frontend.headers.SSLHost=YOUR.SITE.EXAMPLE.COM"
      - "traefik.frontend.headers.STSIncludeSubdomains=true"
      - "traefik.frontend.headers.STSPreload=true"
      - "traefik.frontend.headers.frameDeny=true"
    depends_on:
      - db
      - app

networks:
  internal:
    driver: bridge
  traefik_proxy:
    external:
      name: traefik_proxy

Engines up

Good, now fire it up

docker-compose up -d

And watch it load

docker-compose logs -f

Configuration

From here, if you are installing Matomo, proceed as the instructions dictate at Matomo’s instructions. Remember to put db as the database.

If this is a migration, Matomo accesses the database by it’s name. In config/config.ini.php

[database]
host = "db"

Final checks

Once you’ve got here, be sure to check out the security suggestions. And please respect the privacy of the users that come to your site by enabling the privacy settings. See Matomo’s privacy suggestions