What you will set up
Matomo is the most complete open-source alternative to Google Analytics. Besides visits, pages, sources and locations, it tracks goals, ecommerce orders and revenue, campaigns, site search and events, keeps a log of each visit, sends reports by email, and lets you give colleagues view-only access. All visitor data stays in a database on your own server.
Matomo started in 2007 as Piwik, created by Matthieu Aubry. It is developed by InnoCraft Limited, a company registered in New Zealand that was started by Matomo's creators and also sells Matomo Cloud and paid plugins. Matomo is open source under the GNU General Public License version 3 or later, which we checked in its source code on 14 October 2026.
Here Matomo runs as a pod under a user of its own called matomo, with MariaDB for its data, behind Caddy from the Podman guide. The image is the official matomo image on Docker Hub, maintained by the Matomo community. It comes in two kinds: apache, with Apache and PHP in one container, and fpm, with PHP only, which needs a web server that can read Matomo's files. This guide uses apache: Caddy runs under its own user and cannot read the files of the matomo user, so fpm would need a second web server in the pod. With apache, Caddy only passes requests on, as for every other app.
Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:
- Matomo 5.14.0 started with MariaDB 12.3.3, and the installer and the super user account were done over an SSH tunnel before Caddy made Matomo public.
- A small test website with Matomo's tracking code was visited in a browser. The visit showed the visitor's real address, shortened to its first two parts, and the right city, over IPv4 and IPv6. A forged
X-Forwarded-Forheader was ignored. - 1,200 simulated visits were sent through Matomo's Tracking HTTP API over a week, with real browser user agents, addresses from six countries, referrers, two UTM campaigns, two goals, 34 ecommerce orders, site searches and video events. They are not real people. The hourly timer built the reports, and the numbers matched what was sent, with each goal counted once per visit.
- With Force tracking without cookies on, the browser got no cookies. With Do Not Track support on, a visit that sent
DNT: 1was not recorded. The opt-out on a privacy page stopped all tracking for that browser. - A view-only user accepted an invitation and could see the reports but not the settings. A weekly report arrived by email, in a mail catcher on the server.
- The interface was shown in Swedish, by choice and for a browser set to Swedish. The free Google Analytics importer was installed, but not used, since that needs a Google account.
- A capture of the server's traffic listed every outside service Matomo contacted, and none after step 15.
- Matomo was updated from 5.13.0 to 5.14.0. It was restored from a backup for a newly created
matomouser, with all visits, reports, goals and users, and everything came back by itself after a reboot.
After a reboot, the pod used about 170 MB of memory: MariaDB 130 MB and Matomo 40 MB. With the dashboard in use, Matomo grew to about 150 MB, and while it built the reports for the test visits, the pod peaked at about 420 MB. The 1,200 visits, with about 3,500 pageviews and other actions, added about 1 MB of raw data and 2 MB of reports to the database, so plan for roughly 1 MB of raw data per 1,000 visits. The two images take 960 MB on disk. Steps 1 to 7 took us about 10 minutes.
Matomo, Plausible or Umami? All three keep the visitor data on your server. Matomo does the most: ecommerce revenue, goals with conversion rates, campaigns, a log of each visit, site search, segments, scheduled email reports, users with view-only access, a Google Analytics importer and a Swedish interface. It sets cookies until you switch them off, and its privacy settings are yours to choose, which takes the longest of the three. Plausible and Umami set no cookies from the start and show one simpler dashboard. Umami used about 210 MB of memory in our comparison, Plausible about 650 MB with ClickHouse, and Matomo about 170 MB here, more while it builds reports. If you need ecommerce, campaigns and email reports for a team, pick Matomo; if you want visits and sources with the least work, pick one of the other two.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running;
- an A record and an AAAA record for
analytics.example.compointing at your server; - a website to measure, here
www.example.com.
Matomo recommends 2 CPU cores and 2 GB of memory for up to 100,000 pageviews a month. The examples use analytics.example.com for Matomo and 203.0.113.10 for your server's IPv4 address. Replace them throughout.
1. Create the user
As root:
useradd -m -s /bin/bash matomo
loginctl enable-linger matomo
machinectl shell matomo@
Everything up to step 6 runs as matomo.
2. Create the secret
openssl rand -hex 24 | tr -d '\n' | podman secret create matomo-db-password -
This is the password of Matomo's database account. Podman keeps it, not the files below. MariaDB gets it when it creates the database, and the installer in step 4 fills it in by itself, then writes it to Matomo's settings file, config.ini.php, as Matomo has no other place to read it from.
3. Define the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create matomo.pod:
[Pod]
PodName=matomo
# Only Caddy, on this server, can reach Matomo: the port is not open to the internet.
PublishPort=127.0.0.1:8145:80
[Install]
WantedBy=default.target
Create matomo-db.container:
[Container]
ContainerName=matomo-db
Image=docker.io/library/mariadb:12.3
Pod=matomo.pod
Volume=matomo-db:/var/lib/mysql
Environment=MARIADB_DATABASE=matomo MARIADB_USER=matomo MARIADB_RANDOM_ROOT_PASSWORD=1 MARIADB_AUTO_UPGRADE=1
Secret=matomo-db-password,type=env,target=MARIADB_PASSWORD
Exec=--max-allowed-packet=64M
HealthCmd=healthcheck.sh --connect --innodb_initialized
HealthInterval=10s
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
And matomo-app.container:
[Unit]
Requires=matomo-db.service
After=matomo-db.service
[Container]
ContainerName=matomo-app
Image=docker.io/library/matomo:5.14.0-apache
Pod=matomo.pod
Volume=matomo-config:/var/www/html/config
Volume=matomo-misc:/var/www/html/misc
Volume=matomo-plugins:/var/www/html/custom-plugins
Environment="MATOMO_PLUGIN_DIRS=/var/www/html/custom-plugins/;custom-plugins" MATOMO_PLUGIN_COPY_DIR=/var/www/html/custom-plugins/
Environment=MATOMO_DATABASE_HOST=127.0.0.1 MATOMO_DATABASE_USERNAME=matomo MATOMO_DATABASE_DBNAME=matomo
Secret=matomo-db-password,type=env,target=MATOMO_DATABASE_PASSWORD
HealthCmd=curl -fs -o /dev/null http://127.0.0.1/matomo.php
HealthInterval=10s
Notify=healthy
[Service]
Restart=always
TimeoutStartSec=300
# Rebuild the files Matomo changes at run time, which a new container starts without.
ExecStartPost=-/usr/bin/podman exec -u www-data matomo-app ./console core:create-security-files
ExecStartPost=-/usr/bin/podman exec -u www-data matomo-app ./console custom-matomo-js:update
What the settings do:
- At every start, the image copies Matomo's program files from the image into the container. Only three folders are kept in volumes:
config, with your settings,misc, with the location database from step 4, andcustom-plugins, for plugins you install. So a new image brings new program files, which is how you update in step 17. MATOMO_PLUGIN_DIRSandMATOMO_PLUGIN_COPY_DIRmake Matomo install plugins from its Marketplace intocustom-plugins, so they survive a restart.- The
MATOMO_DATABASE_lines fill in the installer's database page for you. - Matomo changes two kinds of files while it runs: its tracking script,
matomo.js, when you change privacy settings such as cookies, and the.htaccessfiles that stop Apache from serving its settings, cache and code folders. A new container starts without those changes, so the twoExecStartPostlines make them again once Matomo answers. The-lets the container start before Matomo is installed, when the commands cannot run yet. - MariaDB stays on 12.3, a long-term release, as in the MariaDB guide, and
AutoUpdate=registryinstalls its fixes.--max-allowed-packet=64Mlets Matomo write larger reports in one go, as it recommends. Matomo itself stays on the exact version until you change it.
Start it:
systemctl --user daemon-reload
systemctl --user start matomo-pod
systemctl --user enable --now podman-auto-update.timer
The first start downloads about 960 MB of images; on our server it took 70 seconds. Matomo is ready when this prints 200:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8145/
4. Install over an SSH tunnel
Until the installer has finished, anyone who reaches Matomo can run it and create the super user, the account that controls everything. So do it before Caddy makes Matomo public. From your own computer, open an SSH tunnel to the server:
ssh -L 8145:127.0.0.1:8145 root@203.0.113.10
Open http://localhost:8145 in your browser and choose Next through these pages:
- System Check lists what Matomo needs. Everything has a green tick except Forced SSL Connection, which step 6 fixes.
- Database Setup is already filled in from the pod's settings. Set Database Engine to MariaDB and leave the rest.
- Superuser: choose a Superuser login, a long Password and your Email. Leave the two boxes below unticked: they sign you up for email from Matomo.
- Set up a Website: the Website URL field fills in
https://when you click it. Type the rest,www.example.com. Set Website timezone to Sweden, and Ecommerce to Ecommerce enabled if your site sells online. - JavaScript Tracking Code: skip this code, as it points at
localhost. You get the right one in step 9. - Congratulations: leave both boxes ticked. Automatically configure geolocation using a dbip database downloads DB-IP's free city database, about 130 MB, so Matomo can show countries and cities. Anonymize the last byte(s) of visitors IP addresses shortens addresses before they are stored. Choose Continue to Matomo.
The login page appears. Matomo now refuses to run the installer again. Close the tunnel with exit.
5. Trust Caddy
Matomo only accepts requests for host names it knows, and it needs to be told that Caddy passes on the visitor's address and that visitors use HTTPS. As matomo:
podman exec -u www-data matomo-app ./console config:set \
'General.trusted_hosts[]="analytics.example.com"' \
'General.proxy_client_headers[]="HTTP_X_FORWARDED_FOR"' \
'General.proxy_scheme_headers[]="HTTP_X_FORWARDED_PROTO"'
Inside the pod, every request comes from Caddy, at the server's own address. Caddy puts the visitor's real address in X-Forwarded-For, and replaces any value a visitor sends there. Without the second line, every visit has the address 203.0.113.10, and the same location. config:set writes to config.ini.php, and the change takes effect at once.
6. Put Caddy in front
Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:
analytics.example.com {
reverse_proxy 127.0.0.1:8145
}
Restart Caddy with systemctl --user restart caddy, open https://analytics.example.com, and log in. Then, as matomo again (exit, machinectl shell matomo@), make Matomo use HTTPS only:
podman exec -u www-data matomo-app ./console config:set 'General.force_ssl=1'
7. Build reports in the background
Matomo stores each visit as raw data, and builds its reports, the numbers you see, from it in a step it calls archiving. By default, a report is built when someone opens it in the browser. That makes the dashboard slow as visits grow, and Matomo recommends building them on a schedule instead. Switch off archiving in the browser:
podman exec -u www-data matomo-app ./console config:set \
'General.enable_browser_archiving_triggering=0' \
'General.browser_archiving_disabled_enforce=1'
Then run the archiving every hour. Create ~/.config/systemd/user/matomo-archive.service:
[Unit]
Description=Build Matomo's reports
[Service]
Type=oneshot
ExecStart=/usr/bin/podman exec -u www-data matomo-app ./console core:archive --matomo-domain=https://analytics.example.com
And ~/.config/systemd/user/matomo-archive.timer:
[Unit]
Description=Build Matomo's reports every hour
[Timer]
OnCalendar=hourly
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now matomo-archive.timer
systemctl --user start matomo-archive.service
The same run also does Matomo's scheduled tasks, such as sending email reports and deleting old data. For our 1,200 test visits it took 10 seconds. Reports for today are at most an hour old; the Visits Log and Real-time views show visits at once. journalctl --user -u matomo-archive shows how each run went.
8. Choose your privacy settings
Matomo can collect a lot, and the defaults are not the strictest. Open the gear icon, Administration, then Privacy, Anonymize data:
- Anonymize Visitors' IP addresses is already on, with 2 byte(s), from step 4. Matomo stores
198.51.0.0instead of198.51.100.7. Also use the Anonymized IP addresses when enriching visits is No by default: the location is looked up from the full address before it is shortened, which gives cities. With Yes, the location is less precise. - Turn on Force tracking without cookies, and choose Save. Matomo then changes
matomo.jsso that no tracker sets cookies, and ignores tracking cookies on the server. Without it, Matomo sets_pk_id, kept for 13 months,_pk_sesand_pk_ref. Without cookies, a visitor who returns on another day counts as a new visitor. - Under Regularly delete old raw data, tick Regularly delete old raw data from the database, set Delete logs older than (days) to how long you need the details of each visit, such as
180, and choose Save. Matomo asks for your password to confirm changes on this page. The reports stay; only the visit log and the raw data behind it go. Matomo keeps raw data forever by default.
Then Privacy, Users opt-out:
- HTML code to embed on your website gives you a few lines to put on your privacy page. They show a tick box, in the visitor's language, which stores an opt-out in the visitor's browser when it is unticked. Older Matomo versions offered an iframe for this; Matomo 5 uses this script instead.
- Under Support Do Not Track preference, choose Enable Do Not Track support if you want to respect it. Matomo calls the feature deprecated, as few browsers still send the header, but it worked in 5.14.0: a visit with
DNT: 1was not recorded.
What this means for consent, as we read the sources, checked 2026-10-14. It is not legal advice: ask your data protection officer or a lawyer about your own site.
- In Sweden, the rule on cookies is in the Electronic Communications Act, supervised by PTS. As a main rule, cookies may not be stored without consent, except those needed for a service the visitor asked for, and PTS names collecting statistics as something that does not count as needed (PTS, updated 2024-05-14). With cookies on, Matomo needs consent like any other analytics.
- Without cookies, the question is not settled by that alone. The European Data Protection Board's guidelines on the same rule, adopted 7 October 2024, say that a tracking request sent by JavaScript from the visitor's browser can also fall under it, and that this "does not systematically mean that consent needs to be collected" (EDPB Guidelines 2/2023).
- The GDPR still applies to what Matomo stores: the shortened IP address, the browser and the pages are personal data for as long as they can be tied to a person. Shortened addresses, no cookies and deleting raw data make that smaller.
- IMY's decisions against Tele2, CDON, Coop and Dagens industri on 3 July 2023 were about sending data to Google in the United States (IMY). With Matomo on your own server in Sweden, that transfer does not happen.
Matomo also has a page for France's data protection authority, CNIL, which lets sites skip consent for analytics on certain conditions: Privacy, Compliance checks your settings against them. That is French guidance, not Swedish, and our test site met only some of the conditions.
9. Track your website
Under Administration, Websites, Tracking Code, Matomo shows the code for your site. Paste it into the <head> of every page:
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.example.com/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
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);
})();
</script>
<!-- End Matomo Code -->
If the site is served by the same Caddy, a static site needs no more than a block such as www.example.com { root * /srv/www; file_server }, with the site's folder mounted into Caddy's container, as the Plausible guide shows.
Open your site in a browser, then Visitors, Visits Log in Matomo. The visit is there at once, with your address shortened, your city, browser and the pages you opened. What else Matomo tracks, with what we tested:
- Campaigns. Links with
utm_source,utm_mediumandutm_campaign, such ashttps://www.example.com/?utm_source=linkedin&utm_medium=social&utm_campaign=autumn, show up under Acquisition, Campaigns. Matomo's ownmtm_parameters work the same. - Goals. Under Goals, Manage Goals, Add a new goal, a goal is reached when, for example, a visitor opens a URL that contains
thanks.html, or sends an event. Matomo shows conversions, conversion rate and revenue per goal. - Ecommerce. With ecommerce on for the site, your shop's order page adds the items and the order:
<script>
_paq.push(['addEcommerceItem', 'WA-1', 'Widget A', 'Widgets', 499, 1]);
_paq.push(['trackEcommerceOrder', 'ORDER-1001', 499, 499, 99.8, 0, 0]);
</script>
That is SKU, name, category, price and quantity, then order ID, total, subtotal, tax, shipping and discount. Ecommerce then shows orders, revenue and products. Set the site's Currency to Swedish Krona under Websites, Manage.
- Site search. A search page with the term in
q,query,s,searchorkeywordin its address shows the terms under Behaviour, Site Search, with no setup. - Events. A button can send
_paq.push(['trackEvent', 'Video', 'Play', 'Intro']), which shows under Behaviour, Events.
Matomo drops visits from known bots. We sent our simulated visits through the Tracking HTTP API, with a token so they could carry addresses and times of their own; a browser with the snippet above needs nothing more.
10. Give others view-only access
Under Administration, System, Users, choose Invite a new user, enter a Username and Email, pick your website under First website permission, and choose Invite user. The new user gets the View role for that site. Matomo sends an invitation, valid for 7 days, with a link where the user sets a password. A user with View sees every report of that site, but no settings: our test user got "You can't access this resource as it requires admin access" on the settings pages.
Until email works (step 11), the user's row has Resend/Copy Invite Link, which gives you the link to send another way. Write also lets a user add goals and annotations, and Admin change the site's settings and its users. Each user can turn on two-factor authentication under Personal, Security.
11. Email and scheduled reports
Matomo sends email for invitations, password resets, scheduled reports and notices about new versions. This guide does not set up a mail server, and without one:
- invitations are saved but never arrive, so use Resend/Copy Invite Link;
- Lost your password? on the login page says "Could not instantiate mail function";
- scheduled reports are not delivered.
To send through your email provider, store the SMTP password as a secret, as matomo:
printf %s 'the-smtp-password' | podman secret create matomo-smtp-password -
Then give Matomo the settings, with your own server, account and sender:
podman exec -u www-data matomo-app ./console config:set \
'mail.transport="smtp"' 'mail.host="mail.example.com"' 'mail.port=587' \
'mail.encryption="tls"' 'mail.type="Login"' 'mail.username="analytics@example.com"' \
'General.noreply_email_address="analytics@example.com"' 'General.noreply_email_name="Matomo"'
podman exec -u www-data matomo-app ./console config:set \
"mail.password=\"$(podman secret inspect --showsecret --format '{{.SecretData}}' matomo-smtp-password)\"" > /dev/null
podman exec -u www-data matomo-app ./console core:test-email you@example.com
tls on port 587 uses STARTTLS; for port 465, use mail.port=465 and mail.encryption="ssl". Like the database password, Matomo keeps the SMTP password in config.ini.php, and the > /dev/null keeps it off your screen. We tested these settings against a mail catcher on the server, with a login, not against a real provider.
For a weekly report, open Administration, Personal, Schedule Reports, and choose Create and schedule a report: give it a Description, set Report Schedule to Weekly, add other recipients under Also send the report to these emails, tick the reports you want, such as the visits overview, channels, goals and countries, and choose Create Report. The hourly run from step 7 sends it. In our test, the report arrived as an HTML email to both recipients.
12. Use Matomo in Swedish
Matomo comes translated into Swedish. Each user picks Svenska under Personal, Settings, Language, and Save. Users who have not picked a language get the one their browser asks for.
13. Import from Google Analytics
Matomo's free plugin Google Analytics Importer copies your old reports from Google Analytics 4 into Matomo, so that your history does not start from zero. Install it, as matomo:
podman exec -u www-data matomo-app ./console plugin:install-or-update GoogleAnalyticsImporter
podman exec -u www-data matomo-app ./console plugin:activate GoogleAnalyticsImporter
It appears under Administration, System, Google Analytics Import. To use it, you create a project in Google Cloud, switch on the Google Analytics Data API and Admin API, and make an OAuth client of the type Web application, whose settings file you upload there (Matomo's guide). It imports the reports, not the single visits: the Visits Log, segments and ecommerce logs stay empty for the imported period, and goals come without revenue (Matomo). We installed version 5.2.4 and saw its upload page, but did not import anything, as that needs a Google account with Analytics data.
14. Paid plugins
Matomo's Marketplace, under Administration, Plugins, sells plugins that are part of Matomo Cloud but not of the free version. Until step 15, Matomo also shows them as menu items that lead to the shop, such as Funnels, Heatmaps and Session Recordings, even to view-only users. Prices from the Marketplace on 2026-10-14, per year, for up to 4 users, before VAT:
| Plugin | Price |
|---|---|
| Heatmap & Session Recording | €219 |
| A/B Testing | €219 |
| Custom Reports | €219 |
| Funnels | €199 |
| Media Analytics | €179 |
| Advertising Conversion Export | €169 |
| Crash Analytics | €153 |
| Form Analytics | €149 |
| Users Flow, Cohorts, Multi Channel Conversion Attribution, SEO Web Vitals | €89 each |
| Activity Log | €39 |
| Login SAML, for single sign-on | €2,219, any number of users |
Bundles start at €2,750 a year for 9 plugins and up to 4 users. We tested none of them. Free plugins, such as the Google Analytics importer and Marketing Campaigns Reporting, install as in step 13.
15. Turn off outside connections
We captured the server's DNS lookups and outgoing connections while Matomo started, showed the dashboard and the Marketplace, and ran its scheduled tasks. It contacted:
- api.matomo.org, to check for a new version, which then shows as New Update at the top;
- plugins.matomo.org, the Marketplace, for the plugin list, the paid-plugin menu items and plugin updates;
- matomo.org, for the news feed in the dashboard;
- raw.githubusercontent.com, for updated lists of referrer spam, search engines and social networks;
- download.db-ip.com, for a new location database once a month;
- the address of your own website, which Matomo looked up by name; we did not find out which feature does it.
Each of them learns your server's address and when it asked, and none of them gets visitor data. To switch all of them off, as matomo:
podman exec -u www-data matomo-app ./console config:set 'General.enable_internet_features=0'
In a second capture, Matomo made no outside connections at all, the paid-plugin menu items were gone, and the Marketplace said it needs an internet connection. To install a plugin later, set the value to 1 for a moment. You then also watch for new versions yourself, as step 17 explains.
The location database no longer updates by itself either. DB-IP publishes a new one each month, under the Creative Commons Attribution 4.0 licence, which asks that you credit DB-IP if you show its data, such as with a link to https://db-ip.com on pages that show visitors' locations. To update it every month, create ~/geoip-update.sh:
#!/bin/sh
# Downloads this month's DB-IP city database for Matomo.
set -e
podman exec -u www-data matomo-app sh -c 'curl -fsS https://download.db-ip.com/free/dbip-city-lite-$(date +%Y-%m).mmdb.gz | gunzip > misc/DBIP-City.mmdb.new && mv misc/DBIP-City.mmdb.new misc/DBIP-City.mmdb'
Then ~/.config/systemd/user/matomo-geoip.service:
[Unit]
Description=Update Matomo's GeoIP database
[Service]
Type=oneshot
ExecStart=%h/geoip-update.sh
And ~/.config/systemd/user/matomo-geoip.timer:
[Unit]
Description=Update Matomo's GeoIP database every month
[Timer]
OnCalendar=*-*-05 04:00
Persistent=true
[Install]
WantedBy=timers.target
chmod 700 ~/geoip-update.sh
systemctl --user daemon-reload
systemctl --user enable --now matomo-geoip.timer
systemctl --user start matomo-geoip.service
MaxMind's GeoLite2 is the other free database Matomo can use, under Administration, System, Geolocation. It needs a MaxMind account and licence key, and its licence asks you to keep it up to date. We did not test it.
16. Back up
Matomo keeps the visits, reports, users and settings of each site in MariaDB, and its own settings, with the database password and the salt that its logins depend on, in config.ini.php. As matomo, create ~/backup.sh:
#!/bin/sh
# Saves Matomo's database, settings, plugins and database password in ~/backup.
set -e
umask 077
mkdir -p ~/backup
cd ~/backup
podman exec matomo-db sh -c 'mariadb-dump -umatomo -p"$MARIADB_PASSWORD" --single-transaction matomo' | gzip > matomo-db.sql.gz
podman volume export matomo-config > matomo-config.tar
podman volume export matomo-plugins > matomo-plugins.tar
podman secret inspect --showsecret --format '{{.SecretData}}' matomo-db-password > db-password.txt
Run it every night. Create ~/.config/systemd/user/matomo-backup.service:
[Unit]
Description=Back up Matomo
[Service]
Type=oneshot
ExecStart=%h/backup.sh
And ~/.config/systemd/user/matomo-backup.timer:
[Unit]
Description=Back up Matomo every night
[Timer]
OnCalendar=*-*-* 03:00
Persistent=true
[Install]
WantedBy=timers.target
chmod 700 ~/backup.sh
systemctl --user daemon-reload
systemctl --user enable --now matomo-backup.timer
systemctl --user start matomo-backup.service
ls ~/backup
The dump runs while Matomo keeps working. With our test visits it was 0.5 MB compressed; the plugins took 20 MB, nearly all of it the Google Analytics importer. The location database is left out, as it is downloaded again. Copy ~/backup to another machine, for example with restic, and keep it safe: it holds all your statistics and the passwords.
To restore onto a new server, or for a new matomo user, do step 1 and create the three files from step 3, without starting them. Then, as matomo, with the backup in ~/backup:
tr -d '\n' < ~/backup/db-password.txt | podman secret create matomo-db-password -
podman volume create matomo-config
podman volume import matomo-config ~/backup/matomo-config.tar
podman volume create matomo-plugins
podman volume import matomo-plugins ~/backup/matomo-plugins.tar
systemctl --user daemon-reload
systemctl --user start matomo-db
systemctl --user stop matomo-app
gunzip -c ~/backup/matomo-db.sql.gz | podman exec -i matomo-db sh -c 'mariadb -umatomo -p"$MARIADB_PASSWORD" matomo'
systemctl --user start matomo-app
Starting the database starts the whole pod, Matomo too, so stop Matomo before loading the dump. Then set up the timers from steps 7, 15 and 16 again, and run systemctl --user start matomo-geoip.service for the location database. We restored this way after deleting the matomo user and creating it again: all 1,205 visits, the reports, goals, users and the scheduled report came back, and the old passwords and API token worked.
17. Update
MariaDB updates within 12.3 by itself through podman-auto-update.timer. Matomo stays on the version in matomo-app.container, as Matomo's database needs updating along with its files. New versions come about once a month; with step 15, Matomo no longer tells you, so follow Matomo's changelog or its releases on GitHub. The image follows Matomo's releases, sometimes later: on 14 October 2026, Matomo 5.14.1 had been out for ten days, while the image was still at 5.14.0.
To update, back up (step 16), change the version, restart and update the database, as matomo:
systemctl --user start matomo-backup.service
cd ~/.config/containers/systemd
sed -i 's|matomo:5.14.0-apache|matomo:5.14.1-apache|' matomo-app.container
systemctl --user daemon-reload
systemctl --user restart matomo-app
podman exec -u www-data matomo-app ./console core:update --yes
podman image rm docker.io/library/matomo:5.14.0-apache
We updated from 5.13.0 to 5.14.0 this way. That version had no database changes, so core:update answered "Everything is already up to date". Plugins from the Marketplace are updated with plugin:install-or-update and the plugin's name. Matomo 6 is in beta; read its notes before you move to it.
Troubleshooting
Every visit has the server's address, or the same location. proxy_client_headers from step 5 is missing. Check podman exec matomo-app grep proxy config/config.ini.php.
"The specified host is not valid" or a warning about an untrusted hostname. The address in your browser is not in trusted_hosts. Add it as in step 5.
Reports stay empty or old, while the Visits Log has visits. The archiving has not run. Check systemctl --user list-timers and journalctl --user -u matomo-archive, as matomo.
Visitors get _pk_ cookies, although cookies are off. matomo.js is the one from the image. Run podman exec -u www-data matomo-app ./console custom-matomo-js:update, and check that matomo-app.container has the two ExecStartPost lines.
Diagnostics says "Directory to delete: custom-plugins". Matomo's file check does not know the plugin folder from step 3. Leave it: it holds your plugins.
Matomo shows the installer again. The matomo-config volume is empty, for example after a restore without step 16's volume import. Do not run the installer on a server that can be reached from the internet; restore the backup instead, or close the port in Caddy until you are done.