Self-Hosting Matomo Analytics

August 1, 2026

Matomo is a self-hosted alternative to Google Analytics. You control the data, the tracking behavior, and the retention policy. No third-party JavaScript, no data sharing, no privacy concerns for your visitors. This post covers installing Matomo on Debian with nginx and PHP-FPM as the backend, MySQL as the database, and optional MaxMind GeoIP for geographic reporting. In production, Matomo sits behind an nginx reverse proxy that handles TLS termination.

Prerequisites

Before starting, you need:

Installing PHP and Extensions

Matomo requires PHP with several extensions. Install PHP-FPM and the necessary modules:

apt install -y php-fpm php-mysql php-curl php-gd php-xml \
  php-mbstring php-zip php-intl php-opcache php-cli

Verify PHP-FPM is running:

systemctl status php8.2-fpm

The version number varies by Debian release. Check which version was installed:

php -v

Note the version (e.g., 8.2) — you'll need it for the nginx configuration and FPM socket path.

Creating the Database

Create a dedicated database and user for Matomo:

mysql << 'EOF'
CREATE DATABASE matomo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'matomo'@'localhost' IDENTIFIED BY 'matomo-db-password-here';
GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
FLUSH PRIVILEGES;
EOF

Use utf8mb4 — Matomo stores page titles and URLs that may contain multibyte characters.

Downloading and Installing Matomo

Create the installation directory and download Matomo:

mkdir -p /var/www/matomo
mkdir -p /var/matomo/data

Download and extract the latest release:

apt install -y unzip
cd /tmp
curl -L -o matomo-latest.zip https://builds.matomo.org/matomo-latest.zip
unzip -q matomo-latest.zip
cp -a matomo/* /var/www/matomo/
rm -rf matomo matomo-latest.zip

Set ownership to the web server user:

chown -R www-data:www-data /var/www/matomo
chown -R www-data:www-data /var/matomo/data

Configuring nginx

Remove the default site:

rm -f /etc/nginx/sites-enabled/default

Create the Matomo site configuration at /etc/nginx/sites-available/matomo.conf:

server {
    listen 80;
    listen [::]:80;
    server_name analytics.example.com;

    root /var/www/matomo;
    index index.php;

    access_log /var/log/nginx/matomo_access.log;
    error_log /var/log/nginx/matomo_error.log;

    client_max_body_size 64M;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Block access to sensitive files
    location ~ /\.ht {
        deny all;
    }

    location ~ /\.git {
        deny all;
    }

    # Serve static files directly
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # Main location block
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP handling
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }

    # Matomo tracking endpoints
    location = /matomo.php {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location = /piwik.php {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to internal directories
    location ~ ^/(config|tmp|core|lang) {
        deny all;
        return 404;
    }

    # Deny PHP execution in plugins/themes
    location ~ ^/(plugins|themes)/.*\.php$ {
        deny all;
        return 404;
    }

    # Cache tracking JavaScript
    location ~ ^/(matomo|piwik)\.js$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Replace php8.2-fpm.sock with your actual PHP version. Enable the site:

ln -s /etc/nginx/sites-available/matomo.conf /etc/nginx/sites-enabled/matomo.conf
nginx -t
systemctl reload nginx

nginx Configuration Decisions

Static file caching — JavaScript, CSS, and images are served with a 1-year cache and the immutable flag. Matomo includes version hashes in its asset URLs, so cached files are automatically invalidated on updates.

Blocking sensitive directories — the config, tmp, core, and lang directories contain PHP configuration files and internal data. Denying access prevents information disclosure.

Blocking PHP in plugins/themes — prevents execution of uploaded PHP files in directories where only static assets should exist.

fastcgi_read_timeout 300 — Matomo's archive processing can run long. The default 60-second timeout is too short for large datasets.

Running the Web Installer

If this server sits behind a reverse proxy, make sure the proxy is configured to forward traffic to this backend. Then open your browser and navigate to:

https://analytics.example.com/

The Matomo web installer walks through:

  1. System check (verifies PHP extensions and directory permissions)
  2. Database configuration (use localhost, matomo, and the password you set)
  3. Creating the first admin user
  4. Adding the first website to track
  5. Getting the JavaScript tracking code

Save the tracking code — you'll add it to every page you want to track.

Trusted Proxy Configuration

If Matomo runs behind a reverse proxy, it needs to trust the proxy's X-Forwarded-For header to see real visitor IPs. After the web installer completes, edit /var/www/matomo/config/config.ini.php and add:

[General]
proxy_client_headers[] = HTTP_X_FORWARDED_FOR
proxy_host_headers[] = HTTP_X_FORWARDED_HOST
trusted_hosts[] = "analytics.example.com"

Setting Up the Archive Cron Job

Matomo processes raw tracking data into reports through an "archive" process. By default this happens on each page view of the dashboard, which is slow. Set up a cron job to archive in the background:

crontab -u www-data -e

Add:

5 * * * * /usr/bin/php /var/www/matomo/console core:archive --url=https://analytics.example.com/ > /dev/null 2>&1

This runs every hour at 5 minutes past the hour. Then disable browser-triggered archiving in Matomo's settings under System > General Settings > Archiving Settings and set "Archive reports when viewed from the browser" to No.

GeoIP Configuration (Optional)

Matomo can show visitor locations on a map using MaxMind's GeoLite2 database. This requires a free MaxMind account.

Setting Up MaxMind

  1. Create an account at maxmind.com
  2. Generate a license key in your account dashboard

Install the geoipupdate tool:

apt install -y geoipupdate

Configure it at /etc/GeoIP.conf:

AccountID your-account-id
LicenseKey your-license-key
EditionIDs GeoLite2-City GeoLite2-Country GeoLite2-ASN
DatabaseDirectory /usr/share/GeoIP

Run the initial download:

mkdir -p /usr/share/GeoIP
geoipupdate

Configuring Matomo for GeoIP

In Matomo's admin panel, go to System > Geolocation and select "GeoIP 2 (Php)" as the location provider. Set the GeoIP database path to /usr/share/GeoIP/GeoLite2-City.mmdb.

Auto-Updating the GeoIP Database

MaxMind updates the database weekly. Schedule automatic updates:

crontab -e

Add:

0 3 * * 3 /usr/bin/geoipupdate

This updates every Wednesday at 3:00 AM.

Creating an Update Script

For future Matomo upgrades, create /usr/local/bin/matomo-update:

#!/bin/bash
set -e
cd /var/www/matomo

echo "Downloading latest Matomo..."
curl -L -o /tmp/matomo-latest.zip https://builds.matomo.org/matomo-latest.zip

echo "Extracting..."
unzip -q -o /tmp/matomo-latest.zip -d /tmp

echo "Updating files..."
cp -a /tmp/matomo/* /var/www/matomo/

echo "Setting permissions..."
chown -R www-data:www-data /var/www/matomo

echo "Cleaning up..."
rm -rf /tmp/matomo /tmp/matomo-latest.zip

echo "Running database migration..."
sudo -u www-data php /var/www/matomo/console core:update --yes

echo "Done!"
chmod 755 /usr/local/bin/matomo-update

Run matomo-update whenever a new version is available.

Verifying the Installation

# nginx is serving Matomo
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/

# PHP-FPM is processing requests
curl -s http://127.0.0.1/matomo.php | head -5

# Database is accessible
mysql -e "SHOW TABLES FROM matomo;" | head -10

Summary

After completing these steps:

  • Matomo is installed at /var/www/matomo served by nginx and PHP-FPM
  • Analytics data is stored in a dedicated MySQL database
  • The archive cron processes reports hourly in the background
  • GeoIP provides visitor location data (optional)
  • An update script simplifies future version upgrades
  • All visitor data stays on your server — no third-party tracking

Add the Matomo JavaScript snippet to your sites and you'll have full analytics without sending visitor data to Google or anyone else.