# Where Matomo's country flags come from (and how to change them)

> The little flag next to each visitor in Matomo isn't guessed from language or timezone; it's the country your geolocation database resolved from the visitor's IP. So a wrong flag is almost always a geolocation problem, not a flag problem. Here's how country detection actually works, why one looks wrong, and how to swap the icons.

Published: 2026-07-22
Categories: Matomo Tracking
Canonical: https://martez.io/blog/matomo-country-flags-geolocation

---

Open Visitors → Locations in Matomo and every row carries a little country flag. Two questions usually follow: where does that country actually come from, and can I swap the flag for a different icon? Sometimes there's a sharper third one. A visitor you're certain is in Germany is flying the wrong flag entirely, and now you're wondering whether to trust the report at all.

Here's the model that answers all three. The flag is just the icon for whatever ISO 3166-1 country code Matomo resolved for that visit. It isn't drawn from browser timezone or the domain someone arrived on. So a wrong flag is almost always a wrong *country*, and a wrong country is a location-detection problem, not an icon problem.

There's one catch before you go any further: how Matomo works out the country depends on which geolocation provider you've selected. The Default provider guesses from the browser's language header, which is coarse and frequently wrong. The DBIP / GeoIP 2 providers look the visitor's IP up in a real database. If you've never opened Administration → System → Geolocation, you're probably still on Default, so that's the first thing to check.

(And the old forum gripe that the flags were "blurry," navy indistinguishable from black? Long gone. Matomo ships a flat, redesigned set now.)

<Callout type="tip" title="TL;DR">

The flag is just the icon for whatever country Matomo resolved from the visitor's IP, so a wrong flag almost always means a wrong country rather than a broken image file. Check Administration → System → Geolocation first: the Default provider only guesses from the browser's language header, while DBIP / GeoIP 2 looks the real IP up in a database. If you sit behind a proxy or CDN, confirm the visitor's actual IP is reaching Matomo instead of the proxy's. Only touch the flag PNGs once the country is already correct and you just want different artwork.

</Callout>

## Where the flag actually comes from

When you're on a GeoIP 2 provider, the country is the end of a short chain. The tracker (or the HTTP Tracking API) records the request, Matomo looks the client IP up against a geolocation database to resolve Country, Region, and City, and the flag is the icon for the resolved ISO code: `de`, `gb`, `fr`, and so on.

<FlowSteps
  caption="The flag is the last link in the chain. A wrong one almost always means the wrong IP reached Matomo, or the database resolved it badly."
  steps={[
    { title: "Visitor's browser" },
    { title: "Reverse proxy / CDN", arrow: "page request" },
    { title: "Matomo (PHP)", arrow: "X-Forwarded-For" },
    { title: "GeoIP 2 database", arrow: "client IP" },
    { title: "flags/de.png", arrow: "country = de", tone: "ok" },
  ]}
/>

The Default provider skips this chain entirely; it reads the browser language and never touches an IP database. Once you switch to DBIP / GeoIP 2, the visitor's IP becomes the input, and only two things can go wrong from there: the wrong IP arrives, or the database is too weak to resolve it. Work it backwards in that order.

### The reverse-proxy trap (the most common wrong-country cause)

If you self-host behind Nginx, a load balancer, or Cloudflare, and every visitor resolves to the same country (usually wherever your server lives), Matomo isn't misreading the database. It's geolocating your proxy's IP instead of the visitor's, because that's the IP arriving at PHP. The visitor's real address is sitting in a forwarded header Matomo hasn't been told to trust yet.

The fix has two sides: your proxy has to pass the real client IP, and Matomo has to be told which header to read. Matomo picks up forwarded headers when you list them in `config/config.ini.php` under `[General]`. PHP rewrites header names on the way in, so `X-Forwarded-For` arrives as `HTTP_X_FORWARDED_FOR`:

```ini
[General]
; Trust the client IP your proxy forwards.
proxy_client_headers[] = HTTP_X_FORWARDED_FOR

; Cloudflare sends the real visitor IP in its own header instead:
; proxy_client_headers[] = HTTP_CF_CONNECTING_IP

; Matomo 5 reads the LAST IP in a forwarded chain by default.
; If your proxy puts the visitor's IP first, flip this back:
; proxy_ip_read_last_in_list = 0
```

One safety point: only trust these headers from a proxy you actually control. Any request that reaches PHP directly can forge `X-Forwarded-For` and fake its own location, so if Matomo is reachable without going through the proxy, lock that path down.

This is the single biggest source of "all my visitors are in the wrong country" reports, and it's worth ruling out before you touch anything else. If the proxy layer itself is misbehaving, our [reverse-proxy 502 walkthrough](/blog/self-host-matomo-reverse-proxy-502) covers the header plumbing in more detail.

## Make country detection accurate

Once the right IP is reaching Matomo, point it at a decent database. Go to Administration (the gear icon) → System → Geolocation and pick a provider:

| Provider | What it uses | Best for |
| --- | --- | --- |
| Default | Browser language header | Nothing serious; it's a guess, move off it |
| DBIP / GeoIP 2 (PHP) | IP lookup in a bundled database | Shared hosting, no server module needed |
| DBIP / GeoIP 2 (HTTP Server Module) | An Apache or Nginx module at request time | Your own server, fastest lookups |

For almost everyone, DBIP / GeoIP 2 (PHP) is the right starting point. Then set up automatic database updates so the data doesn't go stale: free DB-IP databases refresh monthly, MaxMind GeoLite2 is free with a license key and a download URL, and commercial databases are more precise again. Pick a weekly or monthly cadence and save.

If you want the full database-and-key path (generating a GeoLite2 key, pasting the URL, scheduling the auto-updates), we wrote that up separately in [setting up GeoIP2 / GeoLite2 in Matomo](/blog/matomo-geoip2-geolite2-setup). It's the natural companion to this post.

Now the caveat that trips people up. Region and City reports only populate for visits tracked while a GeoIP 2 database was active. Switch geolocation on today and the columns fill in going forward; they aren't backfilled automatically for visits you already recorded.

"Not automatically" isn't "never," though. Matomo ships a console command that re-geolocates historical visits from the data it still has:

```bash
# Re-attribute geolocation for visits in a date range
php ./console usercountry:attribute 2025-01-01,2026-06-01
```

A few things to know before you run it:

- It walks every matching visit, so it's slow over large ranges.
- Run it with the PHP provider. It can't use the Apache or Nginx server modules, which resolve location by issuing a web request the command can't make.
- It only works from the visit data Matomo still has. If your privacy settings anonymized stored IPs aggressively, how far back it can re-resolve is limited.
- When it finishes, reprocess the affected reports. The command updates the visit log, but archived report tables can keep showing the old country until archiving runs again.

It's exactly the tool you want after you finally configure a real database, or after a bulk [server-side log import](/blog/matomo-import-logs-not-working) where the visits landed before geolocation was dialed in.

## Replace or customize the flag icons

If the country is right and you just want a different flag style (a regional flag, a custom design), the icons live in the Morpheus theme, named by lowercase ISO 3166-1 alpha-2 code:

```
plugins/Morpheus/icons/dist/flags/
   us.png   de.png   gb.png   fr.png   ...
```

They used to live under `plugins/UserCountry/` and moved here when Matomo consolidated its icon assets, so older forum posts pointing at the old path are out of date. The set is built from the [matomo-icons](https://github.com/matomo-org/matomo-icons) repository: SVG sources resized to PNGs 48px tall on a 4:3 ratio, which the UI then renders around 16px tall.

To swap one:

1. Export your replacement as a PNG matching the existing file's dimensions. Open the current `de.png` (or whichever you're replacing) and match it exactly instead of guessing.
2. Name it by ISO code: `de.png` for Germany, `gb.png` for the United Kingdom, `us.png` for the United States.
3. Drop it into `plugins/Morpheus/icons/dist/flags/`, replacing the file that's there.
4. Hard-refresh the Matomo UI (clear the browser cache) to see it.

Two caveats that matter. A Matomo upgrade will overwrite your file, because the `dist/` folder ships with core and gets replaced when you update. Keep a copy of your custom icons and re-apply them after each upgrade; the durable route is to contribute the change upstream to the matomo-icons repo and let it flow back into the build. And I'm not aware of a documented, supported way to override these PNGs from a theme or plugin, so for a single self-hosted instance, replacing the file directly is the pragmatic call. Just treat it as non-permanent.

## Verify it worked

For geolocation, with a provider selected on the Geolocation screen, open Visitors → Locations & Provider and check a recent visit. The country (and region and city, if your database resolves them) should look right.

For custom flags, open Visitors → Locations or the Visitor Map after clearing the cache, and confirm your replacement renders for the relevant country.

One last thing you'll run into: entries like Unknown aren't broken flags. They're placeholders for visits the database couldn't pin to a single country, usually a missing or stale database, or that reverse-proxy IP problem again.

## What we'd actually do

A wrong country is nearly always geolocation, not the flag, so fix the chain in order. First confirm the visitor's real IP reaches Matomo (the proxy header is the usual culprit), then put a proper GeoIP 2 database behind it with auto-updates. For most instances, GeoIP 2 (PHP) with MaxMind GeoLite2 on a scheduled update is the sweet spot: accurate, free, and no server module to babysit. Only reach for the icon files if you genuinely want a different look, and when you do, keep a backup, because the next upgrade will clobber it.

[Martez](/?utm_source=martez&utm_medium=blog&utm_campaign=matomo-country-flags-geolocation) connects Matomo with Meta Ads and Google Ads so ROAS, CLV, and attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. [Join the waitlist](/signup?utm_source=martez&utm_medium=blog&utm_campaign=matomo-country-flags-geolocation) if that's relevant.

The flag is downstream of the IP. Get the IP right and the flag takes care of itself.
