Skip to content

How to Deploy Matomo on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Matomo, the full-featured Google Analytics alternative, with the official Docker image and MariaDB, then set it up for a reverse proxy and cron archiving.

Before you start
  • A VPS with 2 vCPU / 2–4 GB RAM
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain (or subdomain) for the analytics dashboard
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Matomo is

Matomo is the closest self-hosted match for Google Analytics' depth: goals, e-commerce tracking, segments, custom dimensions, campaign attribution, a tag manager, and years of raw data you own. It is PHP on MySQL or MariaDB, licensed GPL-3.0-or-later. Funnels and heatmaps exist too, but as paid premium plugins.

That depth is why this is a difficulty-3 guide. Matomo is not heavy at idle. The extra work is in running it well: finishing the web installer, telling Matomo it sits behind a reverse proxy, and moving report processing ("archiving") from page views to a cron job.

If you only need page views and referrers, Matomo vs Umami and Matomo vs Plausible set out the lighter options, and Deploy Umami on a VPS is the two-container alternative.

Server sizing

The catalog lists 2 GB RAM as the minimum. On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1), the idle stack measured about 138 MB of RAM and about 1.3 GB of disk. That is what Matomo costs with no traffic and no archiving. Plan for the load, not the idle number:

  • 2 GB RAM / 1–2 vCPU for a few low-traffic sites.
  • 4 GB RAM / 2+ vCPU once archiving runs over months of data. The archive job is the peak, and MariaDB wants memory for its buffer pool.
  • Disk: raw visit logs grow with traffic. Start with 40 GB and watch the database volume.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, along with a non-root user and a ufw firewall. If not, start with Docker & Compose on Ubuntu.

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Matomo (Docker Compose)

This follows the Apache example in the official matomo-org/docker repository: the matomo image and mariadb:lts, with passwords read from a .env file. Three changes: the passwords are generated, the app's port is bound to loopback, and there is a database healthcheck (the healthcheck.sh script ships in the official MariaDB image), so the app only starts once MariaDB accepts connections.

mkdir -p ~/matomo && cd ~/matomo
[ -f .env ] || printf 'MARIADB_PASSWORD=%s\nMARIADB_ROOT_PASSWORD=%s\n' \
  "$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > .env
chmod 600 .env
cd ~/matomo
cat > compose.yml <<'YAML'
services:
  db:
    image: mariadb:lts
    command: --max-allowed-packet=64MB
    restart: always
    volumes:
      - db:/var/lib/mysql
    environment:
      - MARIADB_AUTO_UPGRADE=1
      - MARIADB_DATABASE=matomo
      - MARIADB_DISABLE_UPGRADE_BACKUP=1
      - MARIADB_INITDB_SKIP_TZINFO=1
      - MARIADB_PASSWORD=${MARIADB_PASSWORD}
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_USER=matomo
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  app:
    image: matomo
    restart: always
    volumes:
      - matomo:/var/www/html
    depends_on:
      db:
        condition: service_healthy
    environment:
      - MATOMO_DATABASE_ADAPTER=mysql
      - MATOMO_DATABASE_DBNAME=matomo
      - MATOMO_DATABASE_HOST=db
      - MATOMO_DATABASE_PASSWORD=${MARIADB_PASSWORD}
      - MATOMO_DATABASE_TABLES_PREFIX=matomo_
      - MATOMO_DATABASE_USERNAME=matomo
    ports:
      # Loopback only: Caddy is the only way in from outside.
      - "127.0.0.1:8080:80"

volumes:
  db:
  matomo:
YAML

The matomo volume holds the whole /var/www/html tree. That includes config/config.ini.php, which the installer writes and which holds your settings and salt, so keep it with your backups. The MATOMO_DATABASE_* variables pre-fill the installer's database step.

Start the stack and wait for the installer page:

cd ~/matomo
docker compose up -d
timeout 300 bash -c 'until curl -fsS -o /dev/null http://127.0.0.1:8080/; do sleep 3; done'
docker compose ps

HTTPS + domain

Point an A record such as analytics.example.com at the server, wait for it to resolve, then put Caddy in front, following Automatic HTTPS with Caddy:

analytics.example.com {
    reverse_proxy 127.0.0.1:8080
}

Do this before you run the installer. Matomo records the hostname you installed from as a trusted host, and it later complains about requests that arrive under a different name.

Run the web installer

Open https://analytics.example.com. Matomo starts a setup wizard: system check, database setup, super user, first website, then the tracking code. At Database Setup, the official image's README says to enter:

  • Database Server: db
  • Login: matomo
  • Password: the MARIADB_PASSWORD value from ~/matomo/.env
  • Database Name: matomo

Leave the rest at the defaults (the table prefix is already matomo_). Then create the super user. This account can do everything, so give it a long, unique password. Add your first website, and copy the JavaScript tracking code the last step shows into the <head> of your site's pages.

Tell Matomo it is behind a proxy

Caddy terminates TLS and forwards plain HTTP to Matomo. Without extra settings, Matomo sees the proxy's IP as every visitor's IP and thinks it is served over http://. Matomo's reverse-proxy FAQ covers both with settings in the [General] section of config/config.ini.php. The installer has to finish first, because it creates that file:

cd ~/matomo
docker compose exec -T app sh -c 'cat >> /var/www/html/config/config.ini.php' <<'INI'

[General]
assume_secure_protocol = 1
proxy_client_headers[] = HTTP_X_FORWARDED_FOR
proxy_host_headers[] = HTTP_X_FORWARDED_HOST
INI

If the file already has a [General] section, add the three lines to it instead of appending a second one. Also check that trusted_hosts[] in the same section lists analytics.example.com.

Move archiving to cron

By default, Matomo builds reports while someone is viewing the dashboard ("browser-triggered archiving"). That works for tiny sites. Beyond that it makes the dashboard slow and can time out. Matomo's recommendation is to turn off browser archiving and run the core:archive console command on a schedule.

  1. In Matomo, go to Administration → System → General settings and set "Archive reports when viewed from the browser" to No.
  2. Add an hourly job on the host. It runs the console as the web server user inside the container:
5 * * * * cd /home/deploy/matomo && docker compose exec -T -u www-data app php /var/www/html/console core:archive --url=https://analytics.example.com/ > /dev/null

Put it in the crontab of a user in the docker group (crontab -e), and adjust the path to wherever your ~/matomo folder lives.

Securing it

  • Keep port 8080 on loopback. The compose file above does this.
  • Protect the super user with 2FA. Matomo has two-factor authentication in your personal security settings.
  • Give everyone else their own user with view-only or write access on specific sites. Share the super user with no one.
  • Keep .env private. It holds both database passwords.

Backups

You need the database, plus the config/config.ini.php from the matomo volume. Without the config, the dump alone won't restore a working instance.

cd ~/matomo
mkdir -p ~/backups/matomo
set -o pipefail
docker compose exec -T db sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction matomo' | gzip > ~/backups/matomo/matomo-db-$(date +%F).sql.gz
docker compose exec -T app tar czf - -C /var/www/html config > ~/backups/matomo/matomo-config-$(date +%F).tar.gz
cp .env ~/backups/matomo/env-$(date +%F)
ls -lh ~/backups/matomo

set -o pipefail makes a failed dump fail the command, instead of leaving an empty .gz that looks like a backup. --single-transaction gives a consistent dump of the InnoDB tables without stopping tracking. Copy the folder off the box, and restore it once on a scratch machine to prove it works.

Upgrades

cd ~/matomo
docker compose pull
docker compose up -d

Back up first. The image carries the Matomo code into the matomo volume. After an upgrade, Matomo may show a "database upgrade required" screen on the next login. Run it from there, or run it from the console so a large database doesn't hit a browser timeout:

cd ~/matomo
docker compose exec -T -u www-data app php /var/www/html/console core:update --yes

MARIADB_AUTO_UPGRADE=1 lets MariaDB upgrade its own system tables when the lts tag moves to a new release.

Troubleshooting

Every visitor has the same IP or location. The proxy settings are missing. Add proxy_client_headers[] = HTTP_X_FORWARDED_FOR as shown above.

A warning that Matomo is accessed through an untrusted hostname. Add the hostname as a trusted_hosts[] entry in [General].

The dashboard is slow or reports time out. Browser archiving is still on. Turn it off and let the cron job build the reports. The first core:archive run over a lot of history can take a long time, so run it by hand once and watch it.

The installer can't reach the database. The server name is db, the compose service name, not localhost. docker compose logs db shows whether MariaDB finished initialising.

Verification + next steps

You're done when https://analytics.example.com loads over a valid certificate, you can log in as the super user with 2FA, a test visit to your site shows your real IP rather than the proxy's, the hourly core:archive job runs without errors, and an off-box backup of the database and config folder has been restored once.

From there: set up goals for the actions that matter, add the Tag Manager if you want to manage tags without redeploying your site, and consider the consent and anonymisation settings under Privacy for your jurisdiction.

Next steps

How to self-host Matomo →More self-hosted web analytics tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.