How to Deploy Matomo on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-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.
- 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)
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
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_PASSWORDvalue 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.
- In Matomo, go to Administration → System → General settings and set "Archive reports when viewed from the browser" to No.
- 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
.envprivate. 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.