<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Datenschutz – Serverküche</title><link>https://serverkueche.de/tags/datenschutz/</link><description>Datenschutz – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Tue, 04 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/datenschutz/index.xml" rel="self" type="application/rss+xml"/><item><title>Matomo cookielos selbst hosten: Analytics ohne Consent-Banner</title><link>https://serverkueche.de/tutorials/matomo-selbst-hosten/</link><pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/matomo-selbst-hosten/</guid><description>Matomo 5.12 mit Docker &amp; Traefik aufsetzen: datenschutzfreundliche Web-Analytics mit IP-Anonymisierung, ganz ohne Google und ohne Cookie-Banner.</description><content:encoded><![CDATA[<p>Google Analytics kennt jeden deiner Besucher besser als du selbst – und schickt die Daten in die USA. Mit Matomo holst du dir die Web-Analytics zurück auf den eigenen Server: DSGVO-konform, IP-anonymisiert und in vielen Fällen ganz ohne Cookie-Banner.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>Matomo 5.12</strong> in Docker hinter deinem Traefik-Reverse-Proxy, erreichbar unter deiner eigenen (Sub-)Domain mit gültigem HTTPS-Zertifikat. Als Datenbank kommt <strong>MariaDB 11.4</strong> dazu – beides in einem <code>compose.yaml</code>. Du bekommst ein vollwertiges Analytics-Dashboard, das Besucherzahlen, Herkunft, Geräte und beliebte Seiten auswertet – die Daten liegen ausschließlich auf deinem Server.</p>
<p>Der eigentliche Gewinn gegenüber Google Analytics ist der Datenschutz: Wir kontrollieren die IP-Anonymisierung und konfigurieren Matomo so, dass es ohne Cookies auskommt. Das ist die technische Grundlage dafür, dass du in vielen Fällen auf einen nervigen Consent-Banner verzichten kannst. Matomo ist dabei das Vollpaket – wenn du nur wissen willst, welche Seiten wie oft aufgerufen werden, reicht auch das deutlich schlankere <a href="/tutorials/hitkeep-analytics-selbst-hosten/">HitKeep</a>.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Hinweis
  </p>
  <div class="prose-kitchen text-sm">Dieses Tutorial ersetzt keine Rechtsberatung. Ob du konkret einen Cookie-Banner brauchst, hängt von deinem Einsatz ab. Die hier gezeigte Konfiguration (keine Cookies, IP-Anonymisierung, Opt-out-Möglichkeit) ist aber genau die, die Matomo und viele Datenschutzbehörden als Basis für einen einwilligungsfreien Betrieb nennen.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein <a href="/tutorials/ssh-absichern/">abgesicherter Server</a> mit Debian 13 und einem sudo-Benutzer</li>
<li><a href="/tutorials/docker-installieren/">Docker &amp; Docker Compose installiert</a></li>
<li>Ein <a href="/tutorials/reverse-proxy-traefik/">laufender Traefik-Reverse-Proxy</a> mit dem externen Netzwerk <code>proxy</code> und dem Zertifikats-Resolver <code>le</code> – Matomo hängt sich dort ein</li>
<li>Eine (Sub-)Domain, deren A-/AAAA-Record <a href="/tutorials/domain-mit-server-verbinden/">auf deinen Server zeigt</a>, z. B. <code>statistik.DEINE-DOMAIN.de</code></li>
<li>Eine <a href="/tutorials/backups-mit-restic/">funktionierende Backup-Strategie mit Restic</a> – Analytics-Daten sammeln sich über Jahre an, die willst du nicht verlieren</li>
</ul>
<p>Matomo ist genügsam: Für eine kleine bis mittlere Website reicht der VPS 1000 locker. Erst bei sehr viel Traffic oder wenn viele andere Dienste mitlaufen, lohnt der Blick auf mehr RAM – unser <a href="/serverempfehlung/">Server-Rechner</a> hilft dir bei der Einschätzung.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/matomo-selbst-hosten/" data-content-piece="VPS 1000 G12">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 10,36 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Reicht für Matomo neben ein paar weiteren kleinen Diensten.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12-12m?ref=44083" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span><code data-track-voucher="36nc17844976032"
        class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-projektordner-anlegen-und-netzwerk-prüfen">Schritt 1: Projektordner anlegen und Netzwerk prüfen</h3>
<p>Wir legen für Matomo einen eigenen Ordner unter <code>/opt</code> an – so bleibt jeder Dienst sauber getrennt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/matomo <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/matomo</span></span></code></pre></div>
</div>
<p>Matomo muss ins selbe Docker-Netzwerk wie Traefik, damit der Proxy den Container erreicht. Prüfe, dass das Netzwerk <code>proxy</code> aus dem <a href="/tutorials/reverse-proxy-traefik/">Traefik-Tutorial</a> existiert:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network ls <span class="p">|</span> grep proxy</span></span></code></pre></div>
</div>
<p>Du solltest eine Zeile mit <code>proxy</code> sehen. Falls nicht, richte zuerst Traefik ein – ohne den Proxy bekommt Matomo kein Zertifikat.</p>
<h3 id="schritt-2-passwörter-in-eine-env-auslagern">Schritt 2: Passwörter in eine <code>.env</code> auslagern</h3>
<p>Die Datenbank-Passwörter gehören <strong>nicht</strong> ins <code>compose.yaml</code>, sondern in eine separate <code>.env</code>. Wir lassen sie von <code>openssl</code> zufällig erzeugen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;DB_PASSWORD=</span><span class="k">$(</span>openssl rand -hex 16<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;DB_ROOT_PASSWORD=</span><span class="k">$(</span>openssl rand -hex 16<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span> &gt; .env
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> .env</span></span></code></pre></div>
</div>
<p><code>chmod 600</code> sorgt dafür, dass nur <code>root</code> die Datei lesen kann. Wirf einmal einen Blick hinein, damit du die Werte kennst:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat .env</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">DB_PASSWORD=b51024e3f9a7c8d1e2f3a4b5c6d7e8f9
</span></span><span class="line"><span class="cl">DB_ROOT_PASSWORD=1a2b3c4d5e6f70819a0b1c2d3e4f5061</span></span></code></pre></div>
</div>
<h3 id="schritt-3-die-composeyaml-schreiben">Schritt 3: Die <code>compose.yaml</code> schreiben</h3>
<p>Lege die Datei <code>/opt/matomo/compose.yaml</code> mit folgendem Inhalt an. Ersetze im Traefik-Label <strong><code>statistik.DEINE-DOMAIN.de</code> durch deine echte (Sub-)Domain</strong>:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mariadb:11.4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">matomo-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span>--<span class="l">max-allowed-packet=64MB</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">matomo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_USER</span><span class="p">:</span><span class="w"> </span><span class="l">matomo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_ROOT_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db:/var/lib/mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">intern]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">matomo:5.12-apache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">matomo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MATOMO_DATABASE_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MATOMO_DATABASE_ADAPTER</span><span class="p">:</span><span class="w"> </span><span class="l">mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MATOMO_DATABASE_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="l">matomo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MATOMO_DATABASE_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${DB_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MATOMO_DATABASE_DBNAME</span><span class="p">:</span><span class="w"> </span><span class="l">matomo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">app:/var/www/html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">db]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy, intern]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.matomo.rule=Host(`statistik.DEINE-DOMAIN.de`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.matomo.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.matomo.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.matomo.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">intern</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">app:</span></span></span></code></pre></div>
</div>
<p>Das Wichtigste an dieser Datei:</p>
<ul>
<li><strong>Zwei Netzwerke, bewusst getrennt.</strong> Nur der <code>app</code>-Container hängt im <code>proxy</code>-Netz und ist damit von außen erreichbar. Die Datenbank liegt im internen Netz <code>intern</code> und ist <strong>nicht</strong> über Traefik ansprechbar – genau richtig.</li>
<li><strong>Die <code>MATOMO_DATABASE_*</code>-Variablen</strong> füllen den Web-Installer im nächsten Schritt automatisch vor. Du musst die DB-Daten also nicht noch einmal von Hand eintippen.</li>
<li><strong><code>max-allowed-packet=64MB</code></strong> empfiehlt Matomo für die Datenbank, sonst kann der Import großer Berichte abbrechen.</li>
<li><strong>Kein Port nach außen.</strong> Weder Matomo noch die DB veröffentlichen einen Host-Port. Der einzige Weg hinein führt über Traefik und damit über HTTPS.</li>
</ul>
<h3 id="schritt-4-den-stack-starten">Schritt 4: Den Stack starten</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div>
</div>
<p>Beim ersten Start lädt Docker die Images (rund 310 MB Download, entpackt gut 1,3 GB) und legt die Volumes an. Prüfe danach den Status:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME        IMAGE                COMMAND                  SERVICE   CREATED          STATUS          PORTS
</span></span><span class="line"><span class="cl">matomo      matomo:5.12-apache   &#34;/entrypoint.sh apac…&#34;   app       15 seconds ago   Up 13 seconds   80/tcp
</span></span><span class="line"><span class="cl">matomo-db   mariadb:11.4         &#34;docker-entrypoint.s…&#34;   db        15 seconds ago   Up 14 seconds   3306/tcp</span></span></code></pre></div>
</div>
<p>Beide Container sollten <code>Up</code> sein. Traefik erkennt den neuen Container automatisch über die Labels und holt im Hintergrund ein Let&rsquo;s-Encrypt-Zertifikat für deine Domain. Das dauert meist nur wenige Sekunden. Prüfe von deinem eigenen Rechner aus, ob Matomo antwortet:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -sI https://statistik.DEINE-DOMAIN.de <span class="p">|</span> head -1</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">HTTP/2 200</span></span></code></pre></div>
</div>
<p>Ein <code>HTTP/2 200</code> bedeutet: Zertifikat steht, Traefik routet, Matomo läuft. Falls stattdessen ein Zertifikatsfehler kommt, gib Traefik ein paar Sekunden mehr – siehe „Wenn es nicht funktioniert&quot;.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Installer sofort abschließen
  </p>
  <div class="prose-kitchen text-sm">Sobald der Stack läuft, ist deine Matomo-URL öffentlich erreichbar – und der Web-Installer noch offen. Wer zuerst kommt, legt das Admin-Konto an. Klicke den Installer (Schritt 5 und 6) deshalb <strong>direkt im Anschluss</strong> durch, statt die Instanz tagelang unfertig stehen zu lassen. Kannst du das nicht sofort erledigen, schütze die Domain solange temporär – etwa über <a href="/tutorials/reverse-proxy-traefik/">Traefik-BasicAuth</a> oder eine IP-Allowlist, die nur deine eigene Adresse durchlässt.</div>
</div>
<h3 id="schritt-5-den-web-installer-durchklicken">Schritt 5: Den Web-Installer durchklicken</h3>
<p>Öffne <code>https://statistik.DEINE-DOMAIN.de</code> im Browser. Matomo begrüßt dich mit einem achtstufigen Einrichtungsassistenten.</p>
<p><figure class="my-6"><img src="/tutorials/matomo-selbst-hosten/matomo-installer_hu_d554cd431256c6e7.webp" srcset="/tutorials/matomo-selbst-hosten/matomo-installer_hu_cb779c3e6f2a8245.webp 480w, /tutorials/matomo-selbst-hosten/matomo-installer_hu_d554cd431256c6e7.webp 768w, /tutorials/matomo-selbst-hosten/matomo-installer_hu_8d84b7a2787dcee.webp 1200w, /tutorials/matomo-selbst-hosten/matomo-installer_hu_31ffb833ce9eaaf5.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matomo-selbst-hosten/matomo-installer_hu_68a5300c36b81093.webp"
    alt="Matomo-Installationsassistent im Browser, Willkommensseite mit der Schrittliste von „1. Willkommen&quot; bis „8. Glückwunsch&quot;" title="Der Web-Installer von Matomo 5.12 – acht Schritte bis zum fertigen Dashboard"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der Web-Installer von Matomo 5.12 – acht Schritte bis zum fertigen Dashboard</figcaption></figure></p>
<p>Klicke dich durch die ersten Schritte:</p>
<ol>
<li><strong>Willkommen</strong> → <em>Weiter</em>.</li>
<li><strong>Systemprüfung</strong>: Matomo prüft PHP-Version, Erweiterungen und Schreibrechte. Dank des offiziellen Images bleibt die Liste fehlerfrei. Eine gelbe Warnung siehst du trotzdem: <em>Erzwungene SSL Verbindung</em>. Sie ist hier normal und kein Grund abzubrechen – Matomos eigene Option <code>force_ssl</code> ist schlicht noch nicht gesetzt, ausgeliefert wird trotzdem nur über HTTPS. Wir räumen sie in Schritt 7 aus. → <em>Weiter</em>.</li>
<li><strong>Datenbank einrichten</strong>: Die Felder sind bereits ausgefüllt (das erledigen die <code>MATOMO_DATABASE_*</code>-Variablen aus der <code>compose.yaml</code>). Kontrolliere kurz, dass als Server <code>db</code> steht, und klicke <em>Weiter</em>.</li>
<li><strong>Tabellen anlegen</strong>: Matomo legt sein Datenbankschema an. → <em>Weiter</em>.</li>
</ol>
<p>Jetzt legst du deinen <strong>Hauptadministrator</strong> an. Wähle einen Benutzernamen, ein starkes Passwort und eine echte E-Mail-Adresse (über sie kommen Sicherheits- und Update-Hinweise).</p>
<p>Danach richtest du die <strong>erste Website</strong> ein, die du tracken willst. Neben Name und URL ist die <strong>Zeitzone</strong> ein Pflichtfeld: Lässt du sie auf der Vorauswahl stehen, weist das Formular dich mit <em>Zeitzone der Webseite ist nicht gültig</em> zurück. Sie entscheidet, wann für deine Berichte ein neuer Tag beginnt – für eine deutsche Website also <em>Deutschland – Berlin</em>.</p>
<p><figure class="my-6"><img src="/tutorials/matomo-selbst-hosten/matomo-website_hu_3936c1d8e241247.webp" srcset="/tutorials/matomo-selbst-hosten/matomo-website_hu_8a2fdbf1810f52f5.webp 480w, /tutorials/matomo-selbst-hosten/matomo-website_hu_3936c1d8e241247.webp 768w, /tutorials/matomo-selbst-hosten/matomo-website_hu_a4686d3f264b903c.webp 1200w, /tutorials/matomo-selbst-hosten/matomo-website_hu_4efaf7384020a738.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matomo-selbst-hosten/matomo-website_hu_c040438b74f32fc9.webp"
    alt="Formular „Webseite hinzufügen&quot; im Matomo-Installer mit den Feldern für Name, Adresse und Zeitzone der Website" title="Schritt 6: die erste zu trackende Website anlegen – die Zeitzone ist Pflicht"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Schritt 6: die erste zu trackende Website anlegen – die Zeitzone ist Pflicht</figcaption></figure></p>
<h3 id="schritt-6-den-tracking-code-einbauen">Schritt 6: Den Tracking-Code einbauen</h3>
<p>Im vorletzten Schritt zeigt dir Matomo den <strong>JavaScript-Tracking-Code</strong>. Das ist das kleine Snippet, das in jede Seite deiner Website gehört – idealerweise direkt vor <code>&lt;/head&gt;</code>:</p>
<p><figure class="my-6"><img src="/tutorials/matomo-selbst-hosten/matomo-tracking_hu_534bf696465dd87f.webp" srcset="/tutorials/matomo-selbst-hosten/matomo-tracking_hu_5bd94665cc4deb82.webp 480w, /tutorials/matomo-selbst-hosten/matomo-tracking_hu_534bf696465dd87f.webp 768w, /tutorials/matomo-selbst-hosten/matomo-tracking_hu_1f7424b9ebf1c69b.webp 1200w, /tutorials/matomo-selbst-hosten/matomo-tracking_hu_c377490661b56016.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matomo-selbst-hosten/matomo-tracking_hu_4e3bf550cb936faf.webp"
    alt="Matomo-Installer zeigt den fertigen JavaScript-Tracking-Code zum Kopieren an" title="Der Tracking-Code – dieses Snippet kommt in den Head-Bereich deiner Website"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der Tracking-Code – dieses Snippet kommt in den Head-Bereich deiner Website</figcaption></figure></p>
<div class="sk-code">
  <span class="sk-code-head">HTML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- Matomo --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">_paq</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">_paq</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">_paq</span> <span class="o">||</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">  <span class="nx">_paq</span><span class="p">.</span><span class="nx">push</span><span class="p">([</span><span class="s1">&#39;disableCookies&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">_paq</span><span class="p">.</span><span class="nx">push</span><span class="p">([</span><span class="s1">&#39;trackPageView&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">_paq</span><span class="p">.</span><span class="nx">push</span><span class="p">([</span><span class="s1">&#39;enableLinkTracking&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">u</span><span class="o">=</span><span class="s2">&#34;//statistik.DEINE-DOMAIN.de/&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">_paq</span><span class="p">.</span><span class="nx">push</span><span class="p">([</span><span class="s1">&#39;setTrackerUrl&#39;</span><span class="p">,</span> <span class="nx">u</span><span class="o">+</span><span class="s1">&#39;matomo.php&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">_paq</span><span class="p">.</span><span class="nx">push</span><span class="p">([</span><span class="s1">&#39;setSiteId&#39;</span><span class="p">,</span> <span class="s1">&#39;1&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">d</span><span class="o">=</span><span class="nb">document</span><span class="p">,</span> <span class="nx">g</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;script&#39;</span><span class="p">),</span> <span class="nx">s</span><span class="o">=</span><span class="nx">d</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s1">&#39;script&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="nx">g</span><span class="p">.</span><span class="kr">async</span><span class="o">=</span><span class="kc">true</span><span class="p">;</span> <span class="nx">g</span><span class="p">.</span><span class="nx">src</span><span class="o">=</span><span class="nx">u</span><span class="o">+</span><span class="s1">&#39;matomo.js&#39;</span><span class="p">;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">g</span><span class="p">,</span><span class="nx">s</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">})();</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c">&lt;!-- End Matomo Code --&gt;</span></span></span></code></pre></div>
</div>
<p>Die Zeile <code>_paq.push(['disableCookies']);</code> (im Original-Snippet nicht enthalten) haben wir bewusst ergänzt: Damit verzichtet Matomo komplett auf Cookies – ein wichtiger Baustein für den Betrieb ohne Consent-Banner. Betreibst du deine Website mit einem CMS wie WordPress, gibt es fertige Plugins („Matomo for WordPress&quot;, „Connect Matomo&quot;), die das Snippet automatisch einbinden. Nach dem letzten Schritt („Fertig&quot;) meldest du dich mit deinem eben angelegten Konto an und landest im Dashboard.</p>
<p><figure class="my-6"><img src="/tutorials/matomo-selbst-hosten/matomo-dashboard_hu_6c3e00340bcdaff5.webp" srcset="/tutorials/matomo-selbst-hosten/matomo-dashboard_hu_e3134bedcd9f4f4d.webp 480w, /tutorials/matomo-selbst-hosten/matomo-dashboard_hu_6c3e00340bcdaff5.webp 768w, /tutorials/matomo-selbst-hosten/matomo-dashboard_hu_393ce8a7a4434be4.webp 1200w, /tutorials/matomo-selbst-hosten/matomo-dashboard_hu_882486362124748.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matomo-selbst-hosten/matomo-dashboard_hu_c542ba15f54f9421.webp"
    alt="Matomo-Dashboard nach der Installation mit dem Besucherüberblick und dem Verwaltungsmenü" title="Das fertige Matomo-Dashboard – alle Daten liegen auf deinem Server"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das fertige Matomo-Dashboard – alle Daten liegen auf deinem Server</figcaption></figure></p>
<p>Das Dashboard bündelt die wichtigsten Berichte: <em>Besuche in Echtzeit</em>, den <em>Graph der letzten Besuche</em>, die <em>Besucherübersicht</em>, eine <em>Besucherkarte</em> und unter <em>Kanaltypen</em> die Herkunft der Besucher (Suchmaschine, Direktzugriff, Verweis). Über das Menü links kommst du tiefer – <strong>Besucher</strong> (Geräte, Browser, Standort), <strong>Verhalten</strong> (Seiten, Ein- und Ausstiege) und <strong>Akquisition</strong> (welche Kanäle Besucher bringen). Genau die Fragen, die Google Analytics beantwortet – nur eben auf deinem Server.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Admin-Konto absichern
  </p>
  <div class="prose-kitchen text-sm">Dein Matomo-Konto sieht das komplette Besuchsverhalten deiner Website – ein lohnendes Ziel. Vergib ein langes, einmaliges Passwort und aktiviere unter Verwaltung → <em>Persönlich → Sicherheit</em> die <strong>Zwei-Faktor-Authentifizierung</strong> (das Plugin ist bereits an Bord). So kommt selbst bei geleaktem Passwort niemand an deine Daten.</div>
</div>
<h3 id="schritt-7-datenschutz-einstellungen-prüfen-der-wichtige-teil">Schritt 7: Datenschutz-Einstellungen prüfen (der wichtige Teil)</h3>
<p>Matomo 5.12 startet datenschutzfreundlicher, als viele erwarten: Die IP-Anonymisierung ist <strong>ab Werk aktiv</strong>. Trotzdem gehst du die Seite einmal durch – erstens, um es mit eigenen Augen gesehen zu haben, zweitens, weil dort ein Schalter liegt, der dein cookieloses Tracking wirklich absichert. Öffne oben rechts <strong>Verwaltung</strong> (Zahnrad) → im Menü links <strong>Privatsphäre → Daten anonymisieren</strong>.</p>
<p><figure class="my-6"><img src="/tutorials/matomo-selbst-hosten/matomo-datenschutz_hu_55903b7d60128429.webp" srcset="/tutorials/matomo-selbst-hosten/matomo-datenschutz_hu_1e894dab13857d15.webp 480w, /tutorials/matomo-selbst-hosten/matomo-datenschutz_hu_55903b7d60128429.webp 768w, /tutorials/matomo-selbst-hosten/matomo-datenschutz_hu_8a167696f93ecb96.webp 1200w, /tutorials/matomo-selbst-hosten/matomo-datenschutz_hu_b21bee2c85fdd52c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/matomo-selbst-hosten/matomo-datenschutz_hu_2c2d5f1e8974e74c.webp"
    alt="Matomo-Seite „Daten anonymisieren&quot; mit aktivierter IP-Anonymisierung und der Auswahl der maskierten Bytes" title="Privatsphäre → Daten anonymisieren: die IP-Anonymisierung steht bereits auf zwei Bytes"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Privatsphäre → Daten anonymisieren: die IP-Anonymisierung steht bereits auf zwei Bytes</figcaption></figure></p>
<p>Drei Stellschrauben sind entscheidend:</p>
<ul>
<li><strong>Anonymisiere die IP-Adresse von Besuchern</strong>: steht schon auf <em>Ja</em>, maskiert werden standardmäßig <strong>2 byte(s)</strong> – aus <code>203.0.113.45</code> wird also <code>203.0.xxx.xxx</code> (Matomo markiert diese Stufe selbst als <em>Empfohlen</em>). Kontrolliere nur, dass das so geblieben ist; damit ist der einzelne Besucher nicht mehr identifizierbar.</li>
<li><strong>Tracking ohne Cookies erzwingen</strong>: einschalten. Matomo ergänzt dann <code>matomo.js</code> serverseitig um den passenden Code und ignoriert Tracking-Cookies zusätzlich beim Empfang. Das wirkt auch dann noch, wenn irgendwo doch das Original-Snippet ohne <code>disableCookies</code> landet – ein Sicherheitsnetz für den Punkt, an dem dein Consent-Banner-Verzicht hängt.</li>
<li><strong>„Do Not Track&quot;</strong>: liegt unter <em>Privatsphäre → Benutzer Opt-Out</em>. Matomo stuft die Unterstützung inzwischen selbst als <strong>veraltet</strong> ein – weil Browser das Signal ungefragt senden oder gar nicht mehr unterstützen, „empfehlen wir nicht mehr, diese Option zu aktivieren&quot;. Verlass dich also nicht darauf. Der belastbare Weg steht auf derselben Seite: der <strong>Opt-out-Baustein</strong> (iframe oder eigenes Formular), den du in deine Datenschutzerklärung einbaust.</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tipp
  </p>
  <div class="prose-kitchen text-sm">Unter <em>Privatsphäre → Daten anonymisieren → Lösche regelmäßig alte Daten aus der Datenbank</em> kannst du Matomo anweisen, Rohdaten nach z. B. 180 Tagen automatisch zu löschen. Das reduziert Datenmenge <strong>und</strong> Datenschutz-Risiko – die aggregierten Berichte bleiben erhalten.</div>
</div>
<p>Bleibt noch der Schalter, den die Systemprüfung aus Schritt 5 angemahnt hat: <code>force_ssl</code>. Er weist unverschlüsselte Zugriffe in Matomo selbst ab, statt sich allein auf Traefik zu verlassen. In der Oberfläche gibt es ihn nicht, er steht in Matomos <code>config/config.ini.php</code> im Volume:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/matomo
</span></span><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -u www-data app sed -i <span class="s1">&#39;/^\[General\]/a force_ssl = 1&#39;</span> config/config.ini.php</span></span></code></pre></div>
</div>
<p><code>sed</code> hängt die Zeile <code>force_ssl = 1</code> direkt unter den Abschnitt <code>[General]</code>; <code>-u www-data</code> sorgt dafür, dass die Datei ihrem bisherigen Besitzer gehört und Matomo sie weiter schreiben kann. Lade danach das Dashboard neu – unter <em>Verwaltung → Diagnose → Systemprüfung</em> ist die Warnung <em>Erzwungene SSL Verbindung</em> verschwunden.</p>
<h3 id="schritt-8-berichte-per-cron-archivieren">Schritt 8: Berichte per Cron archivieren</h3>
<p>In der Grundeinstellung berechnet Matomo seine Berichte „on the fly&quot;, sobald jemand das Dashboard öffnet. Das wird bei mehr Traffic langsam und ist offiziell nicht empfohlen. Besser: ein Cronjob rechnet die Berichte regelmäßig im Hintergrund vor.</p>
<p>Trage dazu auf dem Host einen Cronjob ein, der alle 15 Minuten das Archivierungskommando im Container ausführt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">crontab -e</span></span></code></pre></div>
</div>
<p>Füge diese Zeile hinzu (Domain anpassen):</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">*/15 * * * * cd /opt/matomo &amp;&amp; docker compose exec -T -u www-data app ./console core:archive --url=https://statistik.DEINE-DOMAIN.de &gt;&gt; /opt/matomo/archive.log 2&gt;&amp;1</span></span></code></pre></div>
</div>
<p>Das Log landet bewusst in <code>/opt/matomo/archive.log</code> und nicht unter <code>/var/log</code>: Läuft dein Cron als sudo-Benutzer (nicht als <code>root</code>), scheitert ein Schreibzugriff auf <code>/var/log</code> still. In <code>/opt/matomo</code> hast du Schreibrechte, und du findest das Log direkt beim Projekt.</p>
<p>Anschließend schaltest du die alte Betriebsart ab: Verwaltung → <em>Allgemeine Einstellungen</em> → <em>Archivierungseinstellungen</em>, dort <strong>„Berichte archivieren, wenn diese im Browser angezeigt werden&quot;</strong> auf <strong>Nein</strong> stellen und speichern. So bleibt das Dashboard auch bei viel Traffic schnell – und die Systemprüfung meldet nicht mehr, dass Browser- und Cron-Archivierung gleichzeitig laufen.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Der Browser zeigt einen Zertifikatsfehler oder <code>404 page not found</code>.</strong> Traefik hat das
Let&rsquo;s-Encrypt-Zertifikat noch nicht geholt, oder der DNS-Record zeigt nicht auf den Server. Prüfe
mit <code>dig statistik.DEINE-DOMAIN.de</code>, dass die IP stimmt, und sieh in die Traefik-Logs. Den genauen
Container-Namen aus deinem Traefik-Setup findest du mit <code>docker ps | grep traefik</code>, dann <code>docker logs &lt;container-name&gt;</code> (bei uns z. B. <code>docker logs traefik-traefik-1</code>). Die HTTP-Challenge
scheitert, wenn Port 80 nicht von außen erreichbar ist – kontrolliere deine
<a href="/tutorials/firewall-ufw-einrichten/">Firewall</a> und die
<a href="/tutorials/netcup-firewall-einrichten/">netcup-Firewall</a>.</p>
<p><strong>Der Installer meldet <code>SQLSTATE... Connection refused</code> oder hängt bei der Datenbank.</strong> MariaDB war
beim ersten Start noch nicht bereit. Gib der DB einen Moment und lade die Seite neu. Prüfe mit
<code>docker compose logs db</code>, ob dort <code>ready for connections</code> steht. Erscheint stattdessen ein
Zugangsfehler, passen <code>DB_PASSWORD</code> in der <code>.env</code> und die bereits angelegte DB nicht mehr zusammen –
dann hilft ein sauberer Neustart mit <code>docker compose down -v</code> (Achtung: löscht die Daten) und
<code>docker compose up -d</code>.</p>
<p><strong>Warnung „Es sieht so aus, als ob die <code>trusted_hosts</code>-Einstellung nicht korrekt ist&quot;.</strong> Matomo
prüft aus Sicherheitsgründen, unter welchem Hostnamen es aufgerufen wird. Die Warnung erscheint,
wenn du die Domain wechselst. Bestätige den korrekten Hostnamen über den Button in der Meldung –
Matomo trägt ihn dann in <code>config/config.ini.php</code> ein.</p>
<p><strong>Im Dashboard tauchen keine Besuche auf.</strong> Der Tracking-Code fehlt, ist falsch eingebaut, oder du
besuchst deine Seite selbst (Matomo ignoriert dich, wenn deine IP ausgeschlossen ist). Öffne deine
Website in einem privaten Fenster und prüfe im Netzwerk-Tab, ob ein Request an <code>matomo.php</code>
rausgeht. In Matomo hilft <em>Verwaltung → Diagnose → Tracking Fehlschläge</em>.</p>
<p><strong>Das Dashboard lädt sehr langsam.</strong> Die On-the-fly-Archivierung rechnet bei jedem Aufruf. Richte
den Archivierungs-Cron aus Schritt 8 ein und stelle die Berichterstellung auf Cron um.</p>

</div>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p><strong>Updates</strong> sind bei Matomo unkritisch, aber regelmäßig fällig – etwa monatlich erscheint eine neue Version. Weil wir den Image-Tag auf <code>5.12</code> gepinnt haben, ziehst du Sicherheits-Patches innerhalb der 5.12-Reihe so:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/matomo <span class="o">&amp;&amp;</span> docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p>Beim ersten Aufruf nach einem Update prüft Matomo, ob ein <strong>Datenbank-Upgrade</strong> nötig ist, und führt es nach einem Klick aus.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Vor einem Major-Upgrade: Backup und Release-Notes
  </p>
  <div class="prose-kitchen text-sm">Für einen Sprung auf eine neue Hauptversion (z. B. 5.12 → 6.x) hebst du den Tag in der <code>compose.yaml</code> an. Ein Major-Upgrade zieht oft umfangreichere Datenbank-Migrationen nach sich, die sich nicht ohne Weiteres zurückrollen lassen. Lies deshalb vorher die Release-Notes und ziehe <strong>unbedingt ein Backup</strong> (siehe unten), bevor du das neue Image startest.</div>
</div>
<p><strong>Backups</strong> umfassen zwei Dinge: die Datenbank (alle Besuchsdaten) und das <code>app</code>-Volume (deine Konfiguration in <code>config/config.ini.php</code>). Erstelle regelmäßig einen Datenbank-Dump …</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -T db sh -c <span class="s1">&#39;exec mariadb-dump -u matomo -p&#34;$MARIADB_PASSWORD&#34; matomo&#39;</span> &gt; /opt/matomo/dump.sql</span></span></code></pre></div>
</div>
<p>… und nimm diesen Dump zusammen mit dem Ordner <code>/opt/matomo</code> in deine <a href="/tutorials/backups-mit-restic/">Restic-Off-Site-Backups</a> auf. Ein netcup-Snapshot des ganzen Servers ist eine gute Ergänzung, aber <a href="/tutorials/netcup-snapshots-scp/">kein Ersatz für ein echtes Off-Site-Backup</a> – liegt beides beim selben Anbieter, sind bei einem Totalausfall auch beide weg.</p>
<p><strong>Laufender Betrieb:</strong> Behalte den Speicherplatz im Auge – bei viel Traffic wächst die Datenbank. Die automatische Löschung alter Rohdaten (Schritt 7) und der Archivierungs-Cron (Schritt 8) halten Matomo dauerhaft schlank und schnell. Damit du merkst, wenn Matomo einmal nicht erreichbar ist, nimm die Instanz in dein <a href="/tutorials/uptime-kuma-monitoring/">Uptime-Kuma-Monitoring</a> auf – ein einfacher HTTP-Check auf deine Statistik-Domain genügt.</p>
<p>Und der wichtigste Betriebs-Hinweis zum Schluss: Matomo lebt vom Datenschutz-Versprechen. Wer den Dienst einmal aufgesetzt hat, sollte die Anonymisierungs- und Lösch-Einstellungen aus Schritt 7 nicht wieder aufweichen – sie sind der Grund, warum du überhaupt ohne Consent-Banner auskommst. Prüfe nach jedem größeren Update kurz, dass sie noch stehen.</p>
]]></content:encoded></item><item><title>HitKeep: datenschutzfreundliche Web-Analytics selbst hosten</title><link>https://serverkueche.de/tutorials/hitkeep-analytics-selbst-hosten/</link><pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/hitkeep-analytics-selbst-hosten/</guid><description>HitKeep mit Docker und Traefik aufsetzen: cookielose, DSGVO-freundliche Website-Statistik auf dem eigenen Server – mit Tracking-Code und Backups.</description><content:encoded><![CDATA[<p>Google Analytics kennt jeder – und genau das ist das Problem: Es schickt die Daten deiner
Besucher zu Google, verlangt einen Cookie-Banner und macht dich erklärungspflichtig. HitKeep
dreht das um: cookielose Statistik auf <strong>deinem</strong> Server, unter <strong>deiner</strong> Domain, ohne dass
je ein Byte an Dritte geht.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Am Ende läuft <strong>HitKeep 2.12.0</strong> als einzelner Container hinter deinem Traefik, erreichbar
unter <code>https://DEINE_DOMAIN</code>. Du bekommst ein Dashboard mit Seitenaufrufen, Besuchern,
Verweildauer, Referrern und Geräten – gespeist von einem winzigen JavaScript-Snippet, das du
in deine Website einbaust. HitKeep arbeitet <strong>cookielos</strong> (kein Consent-Banner nötig) und
speichert alles lokal in einer eingebetteten DuckDB-Datenbank. Das Image ist ein schlankes
Distroless-Image (rund 71 MB), das komplett ohne externe Datenbank auskommt – ideal für einen
kleinen VPS.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Hinweis
  </p>
  <div class="prose-kitchen text-sm">HitKeep ist „cookielos&quot;, weil es Besucher über einen täglich wechselnden Hash statt über einen
gesetzten Cookie wiedererkennt. Das ist deutlich datenschutzfreundlicher als klassisches
Tracking, ersetzt aber <strong>keine</strong> rechtliche Beratung. Ob du komplett ohne Einwilligung
auskommst, hängt von deinem konkreten Einsatz ab – im Zweifel mit einem Datenschutzbeauftragten
klären.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein Server mit <strong>Debian 13</strong> und laufendem Docker (getestet auf einem netcup VPS).</li>
<li>Ein <strong><a href="/tutorials/reverse-proxy-traefik/">Reverse Proxy mit Traefik</a></strong>, der TLS-Zertifikate
per Let&rsquo;s Encrypt holt. HitKeep bringt keinen eigenen HTTPS-Server mit – Traefik übernimmt
die Verschlüsselung. Dieses Tutorial setzt das dort beschriebene <code>proxy</code>-Netz und den
Resolver <code>le</code> voraus.</li>
<li>Eine <strong>(Sub-)Domain</strong>, die auf deinen Server zeigt (A-/AAAA-Record). Wir nutzen im Beispiel
<code>DEINE_DOMAIN</code>.</li>
<li>Die Website, die du messen willst – HitKeep misst jede Seite, in die du das Snippet einbaust.</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/hitkeep-analytics-selbst-hosten/" data-content-piece="VPS 1000 G12">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 10,36 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Ein einzelner Container mit eingebetteter DuckDB – der kleinste vServer reicht dafür locker.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12-12m?ref=44083" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span><code data-track-voucher="36nc17844976032"
        class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(nur Neukunden, keine Domains)</span>
</p></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-jwt-secret-erzeugen">Schritt 1: JWT-Secret erzeugen</h3>
<p>HitKeep signiert die Login-Sitzungen mit einem geheimen Schlüssel. Erzeuge dir einen zufälligen
32-Byte-Wert – <strong>nicht</strong> ausdenken, sondern generieren:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span></span></span></code></pre></div>
</div>
<p>Du bekommst eine 64 Zeichen lange Hex-Zeichenkette. Kopiere sie – sie kommt gleich in die
Konfiguration. Ändert sich dieses Secret später, werden alle offenen Logins ungültig; halte es
also stabil und geheim.</p>
<h3 id="schritt-2-compose-datei-anlegen">Schritt 2: Compose-Datei anlegen</h3>
<p>Lege einen Ordner für den Stack an und wechsle hinein:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/hitkeep <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/hitkeep</span></span></code></pre></div>
</div>
<p>Erstelle die Datei <code>compose.yaml</code>. Ersetze <code>DEINE_DOMAIN</code> durch deine echte Domain und
<code>DEIN_JWT_SECRET</code> durch den Wert aus Schritt 1:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hitkeep</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">pascalebeier/hitkeep:2.12.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hitkeep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_PUBLIC_URL</span><span class="p">:</span><span class="w"> </span><span class="l">https://DEINE_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_JWT_SECRET</span><span class="p">:</span><span class="w"> </span><span class="l">DEIN_JWT_SECRET</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_TRUSTED_PROXIES</span><span class="p">:</span><span class="w"> </span><span class="m">172.16.0.0</span><span class="l">/12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_DB_PATH</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/hitkeep/data/hitkeep.db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_DATA_PATH</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/hitkeep/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_ARCHIVE_PATH</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/hitkeep/archive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HITKEEP_BACKUP_PATH</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/hitkeep/backups</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">hitkeep_data:/var/lib/hitkeep/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">hitkeep_archive:/var/lib/hitkeep/archive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">hitkeep_backups:/var/lib/hitkeep/backups</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.hitkeep.rule=Host(`DEINE_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.hitkeep.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.hitkeep.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.hitkeep.loadbalancer.server.port=8080&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hitkeep_data</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hitkeep_archive</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hitkeep_backups</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Die wichtigsten Stellen im Detail:</p>
<ul>
<li><strong><code>HITKEEP_PUBLIC_URL</code></strong> ist die öffentliche Adresse, unter der HitKeep erreichbar ist. Daraus
baut die Oberfläche später den Tracking-Snippet und die Links. Sie muss <strong>exakt</strong> der URL
entsprechen, unter der du HitKeep aufrufst – sonst kommst du in eine Login-Schleife.</li>
<li><strong><code>HITKEEP_TRUSTED_PROXIES</code></strong> ist der Knackpunkt hinter einem Reverse Proxy: Ohne diesen Wert
sieht HitKeep als Absender jedes Aufrufs nur die interne IP von Traefik – dann landen <strong>alle</strong>
Besucher als ein einziger. <code>172.16.0.0/12</code> deckt die Docker-Netze ab, sodass HitKeep dem
<code>X-Forwarded-For</code>-Header von Traefik vertraut und die echte Besucher-IP auswertet.</li>
<li><strong><code>loadbalancer.server.port=8080</code></strong> sagt Traefik, dass HitKeep intern auf Port 8080 lauscht.
Der Container veröffentlicht selbst keine Ports nach außen – der Zugriff läuft ausschließlich
über Traefik.</li>
<li>Die vier <strong><code>_PATH</code>-Variablen</strong> legen Datenbank, Daten, Archiv und Backups unter
<code>/var/lib/hitkeep/</code> ab; die passenden <strong>Volumes</strong> sorgen dafür, dass sie ein Update oder
einen Neustart überleben.</li>
</ul>
<h3 id="schritt-3-stack-starten-und-auf-tls-warten">Schritt 3: Stack starten und auf TLS warten</h3>
<p>Starte den Container:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div>
</div>
<p>Prüfe nach ein paar Sekunden den Status:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<p>Du solltest den Container als <code>healthy</code> sehen – HitKeep bringt einen eigenen Healthcheck mit:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME      IMAGE                         SERVICE   STATUS                   PORTS
</span></span><span class="line"><span class="cl">hitkeep   pascalebeier/hitkeep:2.12.0   hitkeep   Up 18 minutes (healthy)  7946/tcp, 8080/tcp</span></span></code></pre></div>
</div>
<p>Traefik holt jetzt im Hintergrund das Let&rsquo;s-Encrypt-Zertifikat für deine Domain. Prüfe von
deinem eigenen Rechner aus, dass die Auslieferung des Tracking-Skripts steht:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -sI https://DEINE_DOMAIN/hk.js</span></span></code></pre></div>
</div>
<p>Erwartete Ausgabe (gekürzt) – Status 200 und ein <code>text/javascript</code>-Typ, aggressiv gecacht:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">HTTP/2 200
</span></span><span class="line"><span class="cl">content-type: text/javascript; charset=utf-8
</span></span><span class="line"><span class="cl">cache-control: public, max-age=31536000, immutable</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Achtung
  </p>
  <div class="prose-kitchen text-sm">Bekommst du hier ein <code>404</code> von Traefik oder eine Zertifikatswarnung, warte eine Minute (Let&rsquo;s
Encrypt braucht kurz) und prüfe, dass der A-/AAAA-Record deiner Domain wirklich auf den Server
zeigt. Solange das Zertifikat nicht steht, lädt auch das Snippet im Browser nicht.</div>
</div>
<h3 id="schritt-4-admin-konto-anlegen">Schritt 4: Admin-Konto anlegen</h3>
<p>Rufe <code>https://DEINE_DOMAIN</code> im Browser auf. Beim allerersten Start begrüßt dich HitKeep mit der
Ersteinrichtung. Lege hier dein Administrator-Konto an – Name, E-Mail-Adresse und ein Passwort.
Nimm eine lange Passphrase oder ein per Passwortmanager erzeugtes Zufallspasswort; dieses Konto
sieht sämtliche Statistiken und darf nicht an einem schwachen Passwort hängen:</p>
<p><figure class="my-6"><img src="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-admin-einrichten_hu_17dc7d56166405f8.webp" srcset="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-admin-einrichten_hu_10a7123bb5eaf6f6.webp 480w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-admin-einrichten_hu_17dc7d56166405f8.webp 768w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-admin-einrichten_hu_71deefeea56faa5.webp 1200w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-admin-einrichten_hu_ef56741b81244438.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-admin-einrichten_hu_b641e9ef8bfb6096.webp"
    alt="HitKeep-Ersteinrichtung: Formular zum Anlegen des Administrator-Kontos mit Vor- und Nachname, E-Mail-Adresse und Passwort." title="Beim ersten Aufruf legst du dein Admin-Konto an."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Beim ersten Aufruf legst du dein Admin-Konto an.</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tipp
  </p>
  <div class="prose-kitchen text-sm">Diese Ersteinrichtung ist nur beim allerersten Aufruf offen. Trotzdem gilt: Richte das
Admin-Konto <strong>sofort</strong> nach dem Start ein und lass keinen frisch gestarteten HitKeep-Wizard
ungesichert im Netz stehen.</div>
</div>
<h3 id="schritt-5-website-anlegen-und-tracking-code-holen">Schritt 5: Website anlegen und Tracking-Code holen</h3>
<p>Nach dem Login klickst du links oben bei <strong>Sites</strong> auf das Plus und legst deine Website an –
als Domain trägst du die Domain der Seite ein, die du messen willst (z. B. <code>DEINE_WEBSITE</code>).</p>
<p>Öffne dann die Site-Einstellungen und wechsle auf den Reiter <strong>Tracking</strong>. Dort findest du den
<strong>Live tracking verifier</strong> (der auf den ersten Treffer wartet) und die Konfiguration des
Tracking-Codes samt Tracker-Host:</p>
<p><figure class="my-6"><img src="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-tracking-code_hu_dc6a283eb5b165ae.webp" srcset="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-tracking-code_hu_bc41135cf48424d3.webp 480w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-tracking-code_hu_dc6a283eb5b165ae.webp 768w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-tracking-code_hu_c3d82fe0876941d.webp 1200w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-tracking-code_hu_aa79f4d88d44e8cc.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-tracking-code_hu_87dfd87be9a39e96.webp"
    alt="HitKeep-Tracking-Einstellungen: Live-Verifier wartet auf den ersten Treffer, darunter die Konfiguration des Tracking-Codes mit Tracker-Host und Optionen wie DNT und Web Vitals." title="Der Tracking-Reiter liefert das Snippet und prüft live, ob Treffer ankommen."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Der Tracking-Reiter liefert das Snippet und prüft live, ob Treffer ankommen.</figcaption></figure></p>
<p>Der einzubauende Code besteht aus einer einzigen Zeile. HitKeep braucht <strong>keine Site-ID</strong> im
Snippet – die Zuordnung passiert automatisch über die Domain der Seite, auf der das Skript läuft:</p>
<div class="sk-code">
  <span class="sk-code-head">HTML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">async</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://DEINE_DOMAIN/hk.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Hinweis
  </p>
  <div class="prose-kitchen text-sm">Wichtig ist der Unterschied zwischen <strong>Tracker-Host</strong> und <strong>gemessener Domain</strong>: <code>hk.js</code> wird
von deiner HitKeep-Domain (<code>DEINE_DOMAIN</code>) geladen, zugeordnet wird der Treffer aber der Domain
der besuchten Seite (<code>DEINE_WEBSITE</code>). Beide dürfen unterschiedlich sein – die in HitKeep
angelegte Site muss nur zum Hostnamen der besuchten Seite passen.</div>
</div>
<p>Automatisches Event-Tracking (ausgehende Klicks, Downloads, Formular-Absendungen) ist
standardmäßig aktiv. Optional kannst du auf demselben Reiter „Web Vitals&quot; einschalten, um
Ladezeiten (LCP, INP, CLS, FCP, TTFB) mitzumessen, oder „Collect DNT&quot; aktivieren, wenn du auch
Besucher mit „Do Not Track&quot; zählen willst – aus Datenschutzsicht ist die Voreinstellung (DNT
respektieren) die sauberere.</p>
<h3 id="schritt-6-tracking-code-in-die-website-einbauen">Schritt 6: Tracking-Code in die Website einbauen</h3>
<p>Füge die Snippet-Zeile aus Schritt 5 in den <code>&lt;head&gt;</code> deiner Website ein – bei einer statischen
Seite direkt ins HTML-Template, bei einem CMS in den Kopfbereich bzw. ein „Custom HTML&quot;-Feld.
Durch das <code>async</code>-Attribut blockiert das Skript den Seitenaufbau nicht.</p>
<p>Rufe danach eine Seite deiner Website im Browser auf. Der <strong>Live tracking verifier</strong> aus
Schritt 5 sollte innerhalb weniger Sekunden von „Waiting&quot; auf einen ersten Treffer springen –
das ist die Bestätigung, dass die Kette Website → <code>hk.js</code> → HitKeep steht.</p>
<h3 id="schritt-7-das-dashboard-prüfen">Schritt 7: Das Dashboard prüfen</h3>
<p>Sobald Treffer eintrudeln, füllt sich das Dashboard. Unter <strong>Dashboard</strong> siehst du die
Kennzahlen deiner Website – Live-Besucher, Seitenaufrufe, eindeutige Sitzungen, Absprungrate,
Verweildauer und den Traffic-Verlauf:</p>
<p><figure class="my-6"><img src="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-dashboard_hu_20fed96fa1b9aa0e.webp" srcset="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-dashboard_hu_662414d46c29959.webp 480w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-dashboard_hu_20fed96fa1b9aa0e.webp 768w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-dashboard_hu_4d9ecf744e854a2f.webp 1200w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-dashboard_hu_d35548a457f25bc.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-dashboard_hu_92a92986f140919f.webp"
    alt="HitKeep-Dashboard mit Kennzahlen-Kacheln (Live-Besucher, Seitenaufrufe, eindeutige Sitzungen, Absprungrate) und einem Traffic-Verlaufsdiagramm für den aktuellen Tag." title="Das Dashboard bündelt die wichtigsten Kennzahlen und den Traffic-Verlauf."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Dashboard bündelt die wichtigsten Kennzahlen und den Traffic-Verlauf.</figcaption></figure></p>
<p>Weiter unten listet <strong>Latest Hits</strong> die einzelnen Aufrufe mit Pfad, Zeitpunkt, Referrer und
Gerät auf – hier siehst du auf einen Blick, über welche Suchmaschinen und Verweise deine
Besucher kommen:</p>
<p><figure class="my-6"><img src="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-latest-hits_hu_3e42239dd25f7ebe.webp" srcset="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-latest-hits_hu_4876f6c928f4581e.webp 480w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-latest-hits_hu_3e42239dd25f7ebe.webp 768w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-latest-hits_hu_c8a10421fd2e6fc4.webp 1200w, /tutorials/hitkeep-analytics-selbst-hosten/hitkeep-latest-hits_hu_b3e324401e709011.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/hitkeep-analytics-selbst-hosten/hitkeep-latest-hits_hu_99f80a189f8ddb68.webp"
    alt="HitKeep-Tabelle „Latest Hits&quot; mit einzelnen Seitenaufrufen, Referrern wie Suchmaschinen sowie dem Gerätetyp je Aufruf." title="Latest Hits zeigt jeden einzelnen Aufruf mit Referrer und Gerät."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Latest Hits zeigt jeden einzelnen Aufruf mit Referrer und Gerät.</figcaption></figure></p>
<p>Damit steht deine selbst gehostete Statistik: Jeder Aufruf deiner Website landet direkt in
deiner eigenen Datenbank, ohne Umweg über Dritte.</p>
<h3 id="schritt-8-mehr-als-nur-seitenaufrufe">Schritt 8: Mehr als nur Seitenaufrufe</h3>
<p>Für den Anfang reichen Seitenaufrufe und Referrer – HitKeep kann aber deutlich mehr, und die
Bausteine dafür findest du in der linken Navigation. Ein paar, die sich für die meisten Seiten
lohnen:</p>
<ul>
<li><strong>Goals (Ziele):</strong> Definiere ein Ereignis als Ziel – etwa das Absenden eines Kontaktformulars
oder einen Klick auf „Kaufen&quot;. So misst du nicht nur, <em>wie viele</em> kommen, sondern <em>wie viele
das tun, was du willst</em>.</li>
<li><strong>Funnels (Trichter):</strong> Verkette mehrere Schritte (Startseite → Produktseite → Warenkorb) und
sieh, an welcher Stelle Besucher abspringen.</li>
<li><strong>Events:</strong> Neben den automatisch erfassten Ereignissen (ausgehende Klicks, Downloads,
Formulare) kannst du eigene Ereignisse aus deinem Frontend senden.</li>
<li><strong>Web Vitals:</strong> Schaltest du sie im Tracking-Reiter ein, siehst du echte Ladezeiten deiner
Besucher (LCP, INP, CLS) statt synthetischer Laborwerte.</li>
<li><strong>UTM:</strong> Kampagnen-Parameter (<code>utm_source</code>, <code>utm_medium</code>, <code>utm_campaign</code>) werden ausgewertet –
praktisch, um Newsletter- oder Social-Reichweite auseinanderzuhalten.</li>
<li><strong>Email Reports:</strong> Lass dir eine Zusammenfassung regelmäßig per E-Mail schicken, statt selbst
ins Dashboard schauen zu müssen.</li>
</ul>
<p>Eine einzelne HitKeep-Instanz verwaltet außerdem <strong>beliebig viele Websites</strong>: Über das Plus bei
<strong>Sites</strong> legst du weitere an, jede mit eigenem Snippet und eigenem Dashboard. Du brauchst also
keinen zweiten Container, wenn du mehrere Projekte messen willst.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Der Live-Verifier bleibt auf „Waiting&quot; / keine Treffer im Dashboard.</strong>
Prüfe im Browser (Entwicklertools → Netzwerk), ob <code>hk.js</code> überhaupt geladen wird und der
Sende-Request danach mit Status 2xx zurückkommt. Häufigste Ursachen: Das Snippet steckt nicht
im HTML, die in HitKeep angelegte Site-Domain passt nicht zum Hostnamen der besuchten Seite,
oder ein Ad-/Tracking-Blocker filtert den Aufruf. Da du unter deiner eigenen Domain hostest
(First-Party), greifen die meisten Blocker nicht – manche Listen kennen aber den Pfad <code>hk.js</code>.</p>
<p><strong>Alle Besucher kommen scheinbar von einer einzigen IP.</strong>
Dann fehlt oder greift <code>HITKEEP_TRUSTED_PROXIES</code> nicht: HitKeep sieht nur Traefiks interne IP.
Stelle sicher, dass die Variable gesetzt ist (<code>172.16.0.0/12</code> deckt die Docker-Netze ab) und
starte den Stack neu (<code>docker compose up -d</code>). Danach werten Besucherzahlen und Herkunft wieder
die echte Client-IP aus dem <code>X-Forwarded-For</code>-Header aus.</p>
<p><strong>Traefik liefert 404 oder 502.</strong>
Ein <code>404</code> heißt meist, dass die Router-Regel nicht greift – prüfe, dass <code>Host(...)</code> deine echte
Domain enthält und der Container im <code>proxy</code>-Netz hängt. Ein <code>502</code> deutet auf den falschen Port:
HitKeep lauscht intern auf <strong>8080</strong>, deshalb muss <code>loadbalancer.server.port=8080</code> gesetzt sein.</p>
<p><strong>Nach dem Login landest du wieder auf der Anmeldeseite (Login-Schleife).</strong>
Das ist fast immer eine Diskrepanz bei <code>HITKEEP_PUBLIC_URL</code>: Der Wert muss exakt der Adresse
entsprechen, über die du HitKeep aufrufst (inklusive <code>https://</code>, ohne abschließenden Slash).
Korrigiere die Variable und starte den Container neu.</p>
<p><strong>Der Container startet nicht bzw. ist nicht <code>healthy</code>.</strong>
Sieh in die Logs: <code>docker compose logs -f hitkeep</code>. Ein fehlendes oder leeres
<code>HITKEEP_JWT_SECRET</code> ist eine typische Startbremse – erzeuge eines wie in Schritt 1 und trage
es ein.</p>

</div>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p><strong>Updates:</strong> HitKeep bewegt sich in der 2.x-Reihe zügig weiter – schau etwa monatlich in die
<a href="https://github.com/pascalebeier/hitkeep/releases">Releases</a>. Für ein Update setzt du den neuen
Tag in der <code>compose.yaml</code> (ersetze <code>2.12.0</code> durch die neue Version) und ziehst ihn:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p>Weil die Daten in Volumes liegen, bleiben deine Statistiken dabei erhalten. Pinne die Version
bewusst auf einen festen Tag statt <code>latest</code>, damit ein Neustart dir nicht ungeplant eine neue
Hauptversion unterschiebt.</p>
<p><strong>Backups:</strong> Deine gesamte Statistik steckt in einer eingebetteten DuckDB-Datei (<code>hitkeep.db</code>
samt Write-Ahead-Log <code>hitkeep.db.wal</code>) unter <code>/var/lib/hitkeep/data</code>. Sichere deshalb nicht eine
Einzeldatei, sondern das <strong>komplette Volume <code>hitkeep_data</code></strong> regelmäßig – am saubersten mit
<a href="/tutorials/backups-mit-restic/">restic</a>. Weil diese Dateien im laufenden Betrieb beschrieben
werden, sicherst du sie am konsistentesten, indem du den Container kurz stoppst
(<code>docker compose stop</code>), das Volume sicherst und wieder startest – oder das von HitKeep unter
<code>HITKEEP_BACKUP_PATH</code> abgelegte Backup mit einbeziehst. Ein Backup, das du nie zurückgespielt
hast, ist nur ein Hoffnungsschimmer: Teste die Wiederherstellung einmal auf einem Testsystem.</p>
<p><strong>Aufräumen:</strong> Die Datenbank wächst mit dem Traffic. Behalte die Größe der Volumes im Blick
(<code>docker system df -v</code>) und plane bei viel Traffic genügend Speicher ein.</p>
]]></content:encoded></item></channel></rss>