<?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>Hitkeep – Serverküche</title><link>https://serverkueche.de/en/tags/hitkeep/</link><description>Hitkeep – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Fri, 31 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/hitkeep/index.xml" rel="self" type="application/rss+xml"/><item><title>HitKeep: self-host privacy-friendly web analytics</title><link>https://serverkueche.de/en/tutorials/self-host-hitkeep-analytics/</link><pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/self-host-hitkeep-analytics/</guid><description>Set up HitKeep with Docker and Traefik: cookieless, GDPR-friendly website statistics on your own server – with tracking code and backups.</description><content:encoded><![CDATA[<p>Everyone knows Google Analytics – and that&rsquo;s exactly the problem: it sends your visitors&rsquo; data
to Google, requires a cookie banner and makes you accountable to explain it. HitKeep turns that
around: cookieless statistics on <strong>your</strong> server, under <strong>your</strong> domain, without a single byte
ever going to third parties.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>HitKeep 2.12.0</strong> runs as a single container behind your Traefik, reachable at
<code>https://YOUR_DOMAIN</code>. You get a dashboard with page views, visitors, time on page, referrers
and devices – fed by a tiny JavaScript snippet you embed in your website. HitKeep works
<strong>cookieless</strong> (no consent banner needed) and stores everything locally in an embedded DuckDB
database. The image is a lean distroless image (about 71 MB) that gets by entirely without an
external database – ideal for a small 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>Note
  </p>
  <div class="prose-kitchen text-sm">HitKeep is &ldquo;cookieless&rdquo; because it recognizes visitors via a daily-changing hash instead of a
set cookie. That&rsquo;s significantly more privacy-friendly than classic tracking, but is <strong>no</strong>
substitute for legal advice. Whether you can do entirely without consent depends on your
specific use – when in doubt, clarify it with a data protection officer.</div>
</div>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server with <strong>Debian 13</strong> and running Docker (tested on a netcup VPS).</li>
<li>A <strong><a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a></strong> that fetches TLS
certificates via Let&rsquo;s Encrypt. HitKeep brings no own HTTPS server – Traefik handles the
encryption. This tutorial assumes the <code>proxy</code> network and the resolver <code>le</code> described there.</li>
<li>A <strong>(sub)domain</strong> that points to your server (A/AAAA record). In the example we use
<code>YOUR_DOMAIN</code>.</li>
<li>The website you want to measure – HitKeep measures every page into which you embed the
snippet.</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 · /en/tutorials/self-host-hitkeep-analytics/" 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>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</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 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">A single container with an embedded DuckDB – the smallest vServer is easily enough for that.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-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">
  Go to 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 € voucher</strong> for new netcup customers:</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">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-generate-a-jwt-secret">Step 1: Generate a JWT secret</h3>
<p>HitKeep signs the login sessions with a secret key. Generate a random 32-byte value – <strong>don&rsquo;t</strong>
make one up, generate it:</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>You get a 64-character hex string. Copy it – it goes into the configuration shortly. If this
secret changes later, all open logins become invalid; so keep it stable and secret.</p>
<h3 id="step-2-create-the-compose-file">Step 2: Create the Compose file</h3>
<p>Create a folder for the stack and change into it:</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>Create the file <code>compose.yaml</code>. Replace <code>YOUR_DOMAIN</code> with your real domain and
<code>YOUR_JWT_SECRET</code> with the value from step 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://YOUR_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">YOUR_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(`YOUR_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>The most important points in detail:</p>
<ul>
<li><strong><code>HITKEEP_PUBLIC_URL</code></strong> is the public address under which HitKeep is reachable. The interface
later builds the tracking snippet and the links from it. It must match <strong>exactly</strong> the URL
under which you call HitKeep – otherwise you end up in a login loop.</li>
<li><strong><code>HITKEEP_TRUSTED_PROXIES</code></strong> is the crux behind a reverse proxy: without this value, HitKeep
sees only Traefik&rsquo;s internal IP as the sender of every call – then <strong>all</strong> visitors land as a
single one. <code>172.16.0.0/12</code> covers the Docker networks, so HitKeep trusts Traefik&rsquo;s
<code>X-Forwarded-For</code> header and evaluates the real visitor IP.</li>
<li><strong><code>loadbalancer.server.port=8080</code></strong> tells Traefik that HitKeep listens internally on port
8080. The container itself publishes no ports to the outside – access runs exclusively via
Traefik.</li>
<li>The four <strong><code>_PATH</code> variables</strong> store the database, data, archive and backups under
<code>/var/lib/hitkeep/</code>; the matching <strong>volumes</strong> ensure they survive an update or a restart.</li>
</ul>
<h3 id="step-3-start-the-stack-and-wait-for-tls">Step 3: Start the stack and wait for TLS</h3>
<p>Start the 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>Check the status after a few seconds:</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>You should see the container as <code>healthy</code> – HitKeep brings its own healthcheck:</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 now fetches the Let&rsquo;s Encrypt certificate for your domain in the background. Check from
your own machine that the tracking script is served:</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://YOUR_DOMAIN/hk.js</span></span></code></pre></div>
</div>
<p>Expected output (shortened) – status 200 and a <code>text/javascript</code> type, aggressively cached:</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>Warning
  </p>
  <div class="prose-kitchen text-sm">If you get a <code>404</code> from Traefik or a certificate warning here, wait a minute (Let&rsquo;s Encrypt
needs a moment) and check that the A/AAAA record of your domain really points to the server.
As long as the certificate isn&rsquo;t in place, the snippet won&rsquo;t load in the browser either.</div>
</div>
<h3 id="step-4-create-the-admin-account">Step 4: Create the admin account</h3>
<p>Open <code>https://YOUR_DOMAIN</code> in the browser. On the very first start, HitKeep greets you with the
initial setup. Create your administrator account here – name, email address and a password. Take
a long passphrase or a random password generated by a password manager; this account sees all
statistics and must not hang on a weak password:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-hitkeep-analytics/hitkeep-admin-einrichten_hu_b02649cf40b3be10.webp" srcset="/en/tutorials/self-host-hitkeep-analytics/hitkeep-admin-einrichten_hu_3135860f4f0c0e67.webp 480w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-admin-einrichten_hu_b02649cf40b3be10.webp 768w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-admin-einrichten_hu_d9a8892db681c810.webp 1200w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-admin-einrichten_hu_a483d9f5bb43a510.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-hitkeep-analytics/hitkeep-admin-einrichten_hu_4192495b2f148095.webp"
    alt="HitKeep initial setup: form to create the administrator account with first and last name, email address and password." title="On first launch you create your admin account."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">On first launch you create your admin account.</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>Tip
  </p>
  <div class="prose-kitchen text-sm">This initial setup is only open on the very first call. Still: set up the admin account
<strong>immediately</strong> after the start and don&rsquo;t leave a freshly started HitKeep wizard unsecured on
the net.</div>
</div>
<h3 id="step-5-create-a-website-and-get-the-tracking-code">Step 5: Create a website and get the tracking code</h3>
<p>After logging in, click the plus next to <strong>Sites</strong> at the top left and create your website – as
the domain, enter the domain of the site you want to measure (e.g. <code>YOUR_WEBSITE</code>).</p>
<p>Then open the site settings and switch to the <strong>Tracking</strong> tab. There you find the <strong>live
tracking verifier</strong> (which waits for the first hit) and the configuration of the tracking code
including the tracker host:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-hitkeep-analytics/hitkeep-tracking-code_hu_dc6a283eb5b165ae.webp" srcset="/en/tutorials/self-host-hitkeep-analytics/hitkeep-tracking-code_hu_bc41135cf48424d3.webp 480w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-tracking-code_hu_dc6a283eb5b165ae.webp 768w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-tracking-code_hu_c3d82fe0876941d.webp 1200w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-tracking-code_hu_aa79f4d88d44e8cc.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-hitkeep-analytics/hitkeep-tracking-code_hu_87dfd87be9a39e96.webp"
    alt="HitKeep tracking settings: live verifier waits for the first hit, below it the configuration of the tracking code with tracker host and options like DNT and Web Vitals." title="The tracking tab delivers the snippet and checks live whether hits arrive."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The tracking tab delivers the snippet and checks live whether hits arrive.</figcaption></figure></p>
<p>The code to embed consists of a single line. HitKeep needs <strong>no site ID</strong> in the snippet – the
assignment happens automatically via the domain of the page on which the script runs:</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://YOUR_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>Note
  </p>
  <div class="prose-kitchen text-sm">The difference between the <strong>tracker host</strong> and the <strong>measured domain</strong> is important: <code>hk.js</code> is
loaded from your HitKeep domain (<code>YOUR_DOMAIN</code>), but the hit is assigned to the domain of the
visited page (<code>YOUR_WEBSITE</code>). Both may be different – the site created in HitKeep only has to
match the hostname of the visited page.</div>
</div>
<p>Automatic event tracking (outbound clicks, downloads, form submissions) is active by default.
Optionally, on the same tab, you can enable &ldquo;Web Vitals&rdquo; to also measure load times (LCP, INP,
CLS, FCP, TTFB), or enable &ldquo;Collect DNT&rdquo; if you also want to count visitors with &ldquo;Do Not Track&rdquo;
– from a privacy perspective the default (respect DNT) is the cleaner one.</p>
<h3 id="step-6-embed-the-tracking-code-in-the-website">Step 6: Embed the tracking code in the website</h3>
<p>Add the snippet line from step 5 into the <code>&lt;head&gt;</code> of your website – for a static page directly
into the HTML template, for a CMS into the header area or a &ldquo;Custom HTML&rdquo; field. Thanks to the
<code>async</code> attribute, the script doesn&rsquo;t block the page build.</p>
<p>Then open a page of your website in the browser. The <strong>live tracking verifier</strong> from step 5
should jump from &ldquo;Waiting&rdquo; to a first hit within a few seconds – that&rsquo;s the confirmation that
the chain website → <code>hk.js</code> → HitKeep is in place.</p>
<h3 id="step-7-check-the-dashboard">Step 7: Check the dashboard</h3>
<p>As soon as hits trickle in, the dashboard fills up. Under <strong>Dashboard</strong> you see your website&rsquo;s
key figures – live visitors, page views, unique sessions, bounce rate, time on page and the
traffic trend:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-hitkeep-analytics/hitkeep-dashboard_hu_20fed96fa1b9aa0e.webp" srcset="/en/tutorials/self-host-hitkeep-analytics/hitkeep-dashboard_hu_662414d46c29959.webp 480w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-dashboard_hu_20fed96fa1b9aa0e.webp 768w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-dashboard_hu_4d9ecf744e854a2f.webp 1200w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-dashboard_hu_d35548a457f25bc.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-hitkeep-analytics/hitkeep-dashboard_hu_92a92986f140919f.webp"
    alt="HitKeep dashboard with metric tiles (live visitors, page views, unique sessions, bounce rate) and a traffic trend chart for the current day." title="The dashboard bundles the most important key figures and the traffic trend."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The dashboard bundles the most important key figures and the traffic trend.</figcaption></figure></p>
<p>Further down, <strong>Latest Hits</strong> lists the individual calls with path, time, referrer and device –
here you see at a glance which search engines and referrals your visitors come from:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-hitkeep-analytics/hitkeep-latest-hits_hu_3e42239dd25f7ebe.webp" srcset="/en/tutorials/self-host-hitkeep-analytics/hitkeep-latest-hits_hu_4876f6c928f4581e.webp 480w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-latest-hits_hu_3e42239dd25f7ebe.webp 768w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-latest-hits_hu_c8a10421fd2e6fc4.webp 1200w, /en/tutorials/self-host-hitkeep-analytics/hitkeep-latest-hits_hu_b3e324401e709011.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-hitkeep-analytics/hitkeep-latest-hits_hu_99f80a189f8ddb68.webp"
    alt="HitKeep table &ldquo;Latest Hits&rdquo; with individual page views, referrers like search engines and the device type per call." title="Latest Hits shows every single call with referrer and device."
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Latest Hits shows every single call with referrer and device.</figcaption></figure></p>
<p>With that your self-hosted statistics are in place: every call to your website lands directly in
your own database, without a detour via third parties.</p>
<h3 id="step-8-more-than-just-page-views">Step 8: More than just page views</h3>
<p>For the start, page views and referrers are enough – but HitKeep can do considerably more, and
you find the building blocks in the left navigation. A few that are worth it for most sites:</p>
<ul>
<li><strong>Goals:</strong> define an event as a goal – e.g. the submission of a contact form or a click on
&ldquo;Buy&rdquo;. This way you measure not only <em>how many</em> come, but <em>how many do what you want</em>.</li>
<li><strong>Funnels:</strong> chain several steps (home page → product page → cart) and see at which point
visitors drop off.</li>
<li><strong>Events:</strong> besides the automatically captured events (outbound clicks, downloads, forms), you
can send your own events from your frontend.</li>
<li><strong>Web Vitals:</strong> if you enable them on the tracking tab, you see real load times of your
visitors (LCP, INP, CLS) instead of synthetic lab values.</li>
<li><strong>UTM:</strong> campaign parameters (<code>utm_source</code>, <code>utm_medium</code>, <code>utm_campaign</code>) are evaluated – handy
to tell newsletter from social reach.</li>
<li><strong>Email Reports:</strong> have a summary sent to you regularly by email instead of having to look
into the dashboard yourself.</li>
</ul>
<p>A single HitKeep instance also manages <strong>any number of websites</strong>: via the plus next to
<strong>Sites</strong> you create more, each with its own snippet and its own dashboard. So you don&rsquo;t need a
second container if you want to measure several projects.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The live verifier stays on &ldquo;Waiting&rdquo; / no hits in the dashboard.</strong>
Check in the browser (dev tools → Network) whether <code>hk.js</code> is loaded at all and the send request
afterwards comes back with status 2xx. Most common causes: the snippet isn&rsquo;t in the HTML, the
site domain created in HitKeep doesn&rsquo;t match the hostname of the visited page, or an ad/tracking
blocker filters the call. Since you host under your own domain (first-party), most blockers
don&rsquo;t apply – but some lists know the path <code>hk.js</code>.</p>
<p><strong>All visitors seemingly come from a single IP.</strong>
Then <code>HITKEEP_TRUSTED_PROXIES</code> is missing or not taking effect: HitKeep sees only Traefik&rsquo;s
internal IP. Make sure the variable is set (<code>172.16.0.0/12</code> covers the Docker networks) and
restart the stack (<code>docker compose up -d</code>). After that, visitor numbers and origin again
evaluate the real client IP from the <code>X-Forwarded-For</code> header.</p>
<p><strong>Traefik returns 404 or 502.</strong>
A <code>404</code> usually means the router rule isn&rsquo;t matching – check that <code>Host(...)</code> contains your real
domain and the container is on the <code>proxy</code> network. A <code>502</code> indicates the wrong port: HitKeep
listens internally on <strong>8080</strong>, so <code>loadbalancer.server.port=8080</code> must be set.</p>
<p><strong>After login you land on the login page again (login loop).</strong>
That&rsquo;s almost always a mismatch in <code>HITKEEP_PUBLIC_URL</code>: the value must match exactly the address
through which you call HitKeep (including <code>https://</code>, without a trailing slash). Correct the
variable and restart the container.</p>
<p><strong>The container won&rsquo;t start or isn&rsquo;t <code>healthy</code>.</strong>
Look at the logs: <code>docker compose logs -f hitkeep</code>. A missing or empty <code>HITKEEP_JWT_SECRET</code> is a
typical start blocker – generate one as in step 1 and enter it.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Updates:</strong> HitKeep moves along briskly in the 2.x series – check the
<a href="https://github.com/pascalebeier/hitkeep/releases">releases</a> about monthly. For an update, set
the new tag in the <code>compose.yaml</code> (replace <code>2.12.0</code> with the new version) and pull it:</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>Because the data lives in volumes, your statistics are preserved. Deliberately pin the version
to a fixed tag instead of <code>latest</code>, so a restart doesn&rsquo;t slip you an unplanned new major version.</p>
<p><strong>Backups:</strong> Your entire statistics live in an embedded DuckDB file (<code>hitkeep.db</code> plus the
write-ahead log <code>hitkeep.db.wal</code>) under <code>/var/lib/hitkeep/data</code>. So don&rsquo;t back up a single file,
but the <strong>complete <code>hitkeep_data</code> volume</strong> regularly – cleanest with
<a href="/en/tutorials/restic-backups/">Restic</a>. Because these files are written during operation, you
back them up most consistently by briefly stopping the container (<code>docker compose stop</code>), backing
up the volume and starting again – or by including the backup HitKeep stores under
<code>HITKEEP_BACKUP_PATH</code>. A backup you&rsquo;ve never restored is just a glimmer of hope: test the
restoration once on a test system.</p>
<p><strong>Cleanup:</strong> The database grows with the traffic. Keep an eye on the size of the volumes
(<code>docker system df -v</code>) and plan for enough storage with a lot of traffic.</p>
]]></content:encoded></item></channel></rss>