<?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>Monitoring – Serverküche</title><link>https://serverkueche.de/en/tags/monitoring/</link><description>Monitoring – 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>Tue, 21 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/monitoring/index.xml" rel="self" type="application/rss+xml"/><item><title>Monitoring with Grafana &amp; Prometheus: your server in live dashboards</title><link>https://serverkueche.de/en/tutorials/monitoring-grafana-prometheus/</link><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/monitoring-grafana-prometheus/</guid><description>A complete monitoring stack of Prometheus, node-exporter, cAdvisor and Grafana behind Traefik – with live metrics for host and containers.</description><content:encoded><![CDATA[<p><a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a> tells you <em>whether</em> a service is
running. This stack tells you <em>how</em> it&rsquo;s doing: CPU, RAM, disk, network and the load of each
individual container – as live dashboards with history. We build the combination that has
become the de facto standard in self-hosting: Prometheus collects, Grafana shows.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, a complete <strong>monitoring stack</strong> runs behind your Traefik proxy, reachable at
<code>https://grafana.YOUR_DOMAIN</code> with valid HTTPS. Four building blocks interlock:</p>
<ul>
<li><strong>Prometheus 3.13</strong> – the time-series database. It <em>pulls</em> (scrapes) metrics from the other
services at intervals and stores them with a timestamp.</li>
<li><strong>node-exporter 1.12</strong> – delivers the <strong>host metrics</strong>: CPU, memory, load, disks, network
interfaces.</li>
<li><strong>cAdvisor 0.55</strong> – delivers the <strong>container metrics</strong>: CPU and RAM per running container,
so you see which service is eating.</li>
<li><strong>Grafana 13.1</strong> – the interface: ready-made dashboards that turn the Prometheus data into
graphs and displays.</li>
</ul>
<p>The decisive difference from Uptime Kuma: instead of &ldquo;green/red&rdquo; you get <strong>trends</strong> – you
recognize that the RAM has been slowly filling up for days, <em>before</em> the server swaps. Only
Grafana is reachable from outside; Prometheus, node-exporter and cAdvisor get no public route.</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>How big does the server need to be?
  </p>
  <div class="prose-kitchen text-sm">The stack itself is frugal – in the test it occupied only a few hundred MB of RAM. The
storage hunger comes from Prometheus&rsquo;s <strong>retention time</strong>: the longer you keep metrics and
the more services you monitor, the more space and RAM the time-series database needs. Because
monitoring usually runs <em>in addition</em> to your actual apps, we recommend the VPS 2000 with more
reserve – for a pure test setup the smaller one is enough too.</div>
</div>
<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">
  <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 2000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCores · 16 GB RAM · 512 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €19.24/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Enough RAM and storage to run monitoring alongside your apps.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-2000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   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 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="prerequisites">Prerequisites</h2>
<ul>
<li>Confidence with <strong>Docker Compose</strong> – services, volumes, networks. If that&rsquo;s still new:
<a href="/en/tutorials/docker-compose-basics/">understanding Docker Compose</a>.</li>
<li>A running <strong>Traefik reverse proxy</strong> with the shared <code>proxy</code> network and the Let&rsquo;s Encrypt
resolver <code>le</code> – set up as in <a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a>.</li>
<li>A subdomain <code>grafana.YOUR_DOMAIN</code> whose DNS record (A/AAAA) points to your server IP – see
<a href="/en/tutorials/connect-domain-to-server/">connecting a domain to your server</a>.</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-understand-the-architecture">Step 1: Understand the architecture</h3>
<p>Before we create files, the picture behind it – otherwise you debug blind. Prometheus works
on the <strong>pull principle</strong>: the services don&rsquo;t send their values somewhere, but Prometheus
queries them actively. Every &ldquo;exporter&rdquo; provides its metrics under <code>/metrics</code>, Prometheus
fetches them every 15 seconds and stores them in its time-series database. Grafana in turn
queries Prometheus and draws the graphs from it.</p>
<p>Why pull instead of push at all? Because this way Prometheus always knows whether a target is
<em>alive</em>: if an exporter doesn&rsquo;t respond, that&rsquo;s itself information (<code>up = 0</code>). You need no
agents that actively &ldquo;phone home&rdquo;, and you can simply enter every new target into the
configuration. An exporter is nothing magical – it&rsquo;s a tiny web server that delivers a text
list of current measurements under <code>/metrics</code>.</p>
<p>From this follows the security rule of this setup: node-exporter, cAdvisor and Prometheus have
<strong>no authentication</strong>. So they must <strong>never</strong> stand open on the internet. Prometheus and
cAdvisor we bind only to the internal Docker network <code>monitoring</code>; node-exporter runs in the
<strong>host network</strong> (why is explained in step 4), but stays unreachable from outside thanks to
<a href="/en/tutorials/firewall-ufw-setup/">UFW</a> and the
<a href="/en/tutorials/netcup-firewall-setup/">netcup firewall</a>. Only Grafana is additionally on the
<code>proxy</code> network and gets a Traefik route – the only service with a login and a public address.</p>
<h3 id="step-2-create-the-project-folder-and-configure-prometheus">Step 2: Create the project folder and configure Prometheus</h3>
<p>Create the folder structure:</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 ~/monitoring/prometheus ~/monitoring/grafana/provisioning/datasources
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/monitoring</span></span></code></pre></div>
</div>
<p>Create the Prometheus configuration. It defines <em>whom</em> Prometheus queries at what interval:</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="c"># prometheus/prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">scrape_timeout</span><span class="p">:</span><span class="w"> </span><span class="l">10s</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">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="l">prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;localhost:9090&#34;</span><span class="p">]</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">job_name</span><span class="p">:</span><span class="w"> </span><span class="l">node</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;host.docker.internal:9100&#34;</span><span class="p">]</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">job_name</span><span class="p">:</span><span class="w"> </span><span class="l">cadvisor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;cadvisor:8080&#34;</span><span class="p">]</span></span></span></code></pre></div>
</div>
<p>Important: <code>cadvisor:8080</code> appears as a <strong>service name</strong> – Docker resolves it automatically in
the shared <code>monitoring</code> network. The node-exporter, on the other hand, runs in the <strong>host
network</strong> (step 4 explains why) and is therefore addressed via <code>host.docker.internal:9100</code> –
Prometheus gets this name mapped to the Docker host&rsquo;s IP via <code>extra_hosts</code> shortly.
<code>scrape_interval: 15s</code> is a good compromise – often enough for meaningful curves, seldom
enough to hardly create load.</p>
<h3 id="step-3-provision-the-grafana-data-source-automatically">Step 3: Provision the Grafana data source automatically</h3>
<p>Instead of clicking the Prometheus data source into Grafana by hand later, we set it up via
<strong>provisioning</strong> – as a file. That&rsquo;s reproducible: after a rebuild, everything is immediately
back, without clicking.</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="c"># grafana/provisioning/datasources/datasource.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="m">1</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">datasources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">access</span><span class="p">:</span><span class="w"> </span><span class="l">proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">http://prometheus:9090</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">isDefault</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">editable</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span></span></span></code></pre></div>
</div>
<p><code>access: proxy</code> means that <strong>Grafana itself</strong> (server-side) queries Prometheus – the user&rsquo;s
browser never talks to Prometheus directly. That&rsquo;s exactly why Prometheus can stay internal.</p>
<h3 id="step-4-the-compose-stack">Step 4: The Compose stack</h3>
<p>Now the heart. Create the <code>compose.yaml</code>. Replace <code>grafana.YOUR_DOMAIN</code> and the Grafana
password:</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">prometheus</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">prom/prometheus:v3.13.1</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">prometheus</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></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--config.file=/etc/prometheus/prometheus.yml&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--storage.tsdb.path=/prometheus&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--storage.tsdb.retention.time=30d&#34;</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">./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">prom_data:/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">extra_hosts</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;host.docker.internal:host-gateway&#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">monitoring]</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">node-exporter</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">prom/node-exporter:v1.12.1</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">node-exporter</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></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--path.rootfs=/host&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pid</span><span class="p">:</span><span class="w"> </span><span class="l">host</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="s2">&#34;/:/host:ro,rslave&#34;</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">cadvisor</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">gcr.io/cadvisor/cadvisor:v0.55.1</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">cadvisor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">privileged</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">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/kmsg</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">/:/rootfs:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run:/var/run:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/sys:/sys:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/lib/docker/:/var/lib/docker:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/disk/:/dev/disk:ro</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">monitoring]</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">grafana</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">grafana/grafana:13.1.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">grafana</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">prometheus]</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">GF_SECURITY_ADMIN_USER</span><span class="p">:</span><span class="w"> </span><span class="l">admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_SECURITY_ADMIN_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">YOUR_GRAFANA_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_SERVER_ROOT_URL</span><span class="p">:</span><span class="w"> </span><span class="l">https://grafana.YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_USERS_ALLOW_SIGN_UP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</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">grafana_data:/var/lib/grafana</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./grafana/provisioning:/etc/grafana/provisioning:ro</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.grafana.rule=Host(`grafana.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.grafana.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.grafana.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.grafana.loadbalancer.server.port=3000&#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">monitoring, proxy]</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="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">prom_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">grafana_data</span><span class="p">:</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">monitoring</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>What matters here:</p>
<ul>
<li><strong><code>--storage.tsdb.retention.time=30d</code></strong> keeps metrics for 30 days. More = more meaning, but
also more storage. 30 days is a good start.</li>
<li><strong>node-exporter</strong> needs the <strong>host root directory</strong> (<code>/:/host:ro,rslave</code>), <code>pid: host</code>
<strong>and <code>network_mode: host</code></strong> to read real host values instead of container values.
<code>--path.rootfs=/host</code> tells it where the host view is (read-only, <code>:ro</code>). The host network
isn&rsquo;t a nice-to-have: <code>/proc/net</code> is bound to the network namespace – in a normal bridge
network, node-exporter would see <strong>only its own container interface</strong> (effectively just the
scrape traffic), not the real host interfaces. The price: node-exporter thus listens on port
<code>9100</code> of the host – your <a href="/en/tutorials/firewall-ufw-setup/">firewall</a> keeps the port closed
from outside, and Prometheus reaches it via the <code>extra_hosts</code> entry (<code>host-gateway</code>).</li>
<li><strong>cAdvisor</strong> needs <code>privileged: true</code>, the device <code>/dev/kmsg</code> and read mounts on <code>/sys</code> and
<code>/var/lib/docker</code> to detect all containers. That&rsquo;s unavoidable for container monitoring –
that&rsquo;s why cAdvisor stays strictly internal.</li>
<li><strong>Only <code>grafana</code></strong> carries Traefik labels and is on the <code>proxy</code> network. The internal port is
<strong>3000</strong>. <code>GF_SERVER_ROOT_URL</code> must be the public HTTPS address, otherwise logins and
redirects break behind the proxy.</li>
<li><strong><code>GF_USERS_ALLOW_SIGN_UP: &quot;false&quot;</code></strong> prevents strangers from creating their own account. Set
a <strong>strong admin password</strong> – Grafana is publicly accessible.</li>
</ul>
<h3 id="step-5-start-and-check-the-prometheus-targets">Step 5: Start and check the Prometheus targets</h3>
<p>Start the complete stack:</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>The first time, Docker downloads four images – that takes a moment. Check that all containers
are running:</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>All four services (node-exporter, cadvisor, prometheus, grafana) should be <code>running</code>. Now the
decisive test: <strong>Does Prometheus see its targets?</strong> Since Prometheus stays internal, we query
it from a short-lived container that&rsquo;s on the same network (Compose calls the network
<code>monitoring_monitoring</code> – project folder plus network name):</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 run --rm --network monitoring_monitoring curlimages/curl:8.21.0 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -s http://prometheus:9090/api/v1/query?query<span class="o">=</span>up</span></span></code></pre></div>
</div>
<p>In the JSON response there&rsquo;s a <code>&quot;value&quot;</code> with <code>&quot;1&quot;</code> for each target – that means &ldquo;reachable&rdquo;.
In the test all three jobs (<code>prometheus</code>, <code>node</code>, <code>cadvisor</code>) returned a <code>1</code>. A <code>0</code> means
Prometheus can&rsquo;t scrape the target – then the section &ldquo;When things go wrong&rdquo; helps.</p>
<h3 id="step-6-open-grafana-and-import-dashboards">Step 6: Open Grafana and import dashboards</h3>
<p>Open <code>https://grafana.YOUR_DOMAIN</code>. Traefik fetches the certificate on the first access (a few
seconds). Log in with <code>admin</code> and your password from the <code>compose.yaml</code>. Thanks to
provisioning, the Prometheus data source is already connected – you find it under
<strong>Connections → Data sources</strong>:</p>
<p><figure class="my-6"><img src="/en/tutorials/monitoring-grafana-prometheus/grafana-datasource_hu_399dc2e7f48b1788.webp" srcset="/en/tutorials/monitoring-grafana-prometheus/grafana-datasource_hu_7e0a1a479133fa45.webp 480w, /en/tutorials/monitoring-grafana-prometheus/grafana-datasource_hu_399dc2e7f48b1788.webp 768w, /en/tutorials/monitoring-grafana-prometheus/grafana-datasource_hu_c28443e38bd35aa1.webp 1200w, /en/tutorials/monitoring-grafana-prometheus/grafana-datasource_hu_b685c9265d189cbd.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/monitoring-grafana-prometheus/grafana-datasource_hu_2947ead1a73e98f1.webp"
    alt="The provisioned Prometheus data source in Grafana with the internal URL http://prometheus:9090" title="The data source is provisioned via file – Grafana reports it as preconfigured"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The data source is provisioned via file – Grafana reports it as preconfigured</figcaption></figure></p>
<p>Now the dashboards. Instead of building panels ourselves, we import two proven ones from the
Grafana community. Go to <strong>Dashboards → New → Import</strong>, enter the ID and click <strong>Load</strong>:</p>
<p><figure class="my-6"><img src="/en/tutorials/monitoring-grafana-prometheus/grafana-import_hu_daeb0cdee97f29d5.webp" srcset="/en/tutorials/monitoring-grafana-prometheus/grafana-import_hu_ebf84a9b89d945c1.webp 480w, /en/tutorials/monitoring-grafana-prometheus/grafana-import_hu_daeb0cdee97f29d5.webp 768w, /en/tutorials/monitoring-grafana-prometheus/grafana-import_hu_b8420823a49d711c.webp 1200w, /en/tutorials/monitoring-grafana-prometheus/grafana-import_hu_26166f8d6195fe6c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/monitoring-grafana-prometheus/grafana-import_hu_c29ed6a2a42cab3a.webp"
    alt="The Grafana import dialog loads the &ldquo;Node Exporter Full&rdquo; dashboard from Grafana.com" title="Import a dashboard by ID – here \&#34;Node Exporter Full\&#34; (ID 1860)"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Import a dashboard by ID – here \&#34;Node Exporter Full\&#34; (ID 1860)</figcaption></figure></p>
<p>Import these two:</p>
<ul>
<li><strong>1860</strong> – &ldquo;Node Exporter Full&rdquo;: the comprehensive host dashboard.</li>
<li><strong>19792</strong> – &ldquo;cAdvisor Dashboard&rdquo;: metrics per container.</li>
</ul>
<p>On import, select the <strong>Prometheus</strong> data source each time. After that, &ldquo;Node Exporter Full&rdquo;
shows your host in real time – CPU usage, used RAM, disk, network, uptime:</p>
<p><figure class="my-6"><img src="/en/tutorials/monitoring-grafana-prometheus/grafana-node-dashboard_hu_caaac511989edd0f.webp" srcset="/en/tutorials/monitoring-grafana-prometheus/grafana-node-dashboard_hu_96720c854316db6f.webp 480w, /en/tutorials/monitoring-grafana-prometheus/grafana-node-dashboard_hu_caaac511989edd0f.webp 768w, /en/tutorials/monitoring-grafana-prometheus/grafana-node-dashboard_hu_b90af51a41705c01.webp 1200w, /en/tutorials/monitoring-grafana-prometheus/grafana-node-dashboard_hu_3e6bb19e3a185f34.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/monitoring-grafana-prometheus/grafana-node-dashboard_hu_d1052dba15ec2b7e.webp"
    alt="The Grafana dashboard &ldquo;Node Exporter Full&rdquo; shows live values of the server: CPU 1 percent, RAM 14.7 percent used, 4 CPU cores, 8 GB RAM, uptime 2 days" title="Node Exporter Full with real live data from the test server"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Node Exporter Full with real live data from the test server</figcaption></figure></p>
<p>The cAdvisor dashboard breaks the same load down to <strong>individual containers</strong> – here you
immediately see which service draws how much CPU and RAM:</p>
<p><figure class="my-6"><img src="/en/tutorials/monitoring-grafana-prometheus/grafana-container-dashboard_hu_69ee0d4d1a780625.webp" srcset="/en/tutorials/monitoring-grafana-prometheus/grafana-container-dashboard_hu_9f644b6b9b88fc4f.webp 480w, /en/tutorials/monitoring-grafana-prometheus/grafana-container-dashboard_hu_69ee0d4d1a780625.webp 768w, /en/tutorials/monitoring-grafana-prometheus/grafana-container-dashboard_hu_db3e2021d7ef215.webp 1200w, /en/tutorials/monitoring-grafana-prometheus/grafana-container-dashboard_hu_41bcb30176acfef2.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/monitoring-grafana-prometheus/grafana-container-dashboard_hu_412631eaba466c56.webp"
    alt="The cAdvisor dashboard shows CPU consumption per container – each color is a container like grafana, prometheus or traefik" title="cAdvisor: CPU and memory consumption per container, broken down by name"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">cAdvisor: CPU and memory consumption per container, broken down by name</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>Provision dashboards too
  </p>
  <div class="prose-kitchen text-sm">Like the data source, dashboards can also be provisioned via file (under
<code>grafana/provisioning/dashboards/</code> a <code>.yml</code> plus the dashboard JSONs). For getting started, the
import via ID is faster; once your setup is in place, provisioning is worth it so all dashboards
are automatically back after a rebuild.</div>
</div>
<h3 id="step-7-read-the-most-important-metrics-correctly">Step 7: Read the most important metrics correctly</h3>
<p>A dashboard full of curves is of little use if you don&rsquo;t know what to watch for. These five
values in the &ldquo;Node Exporter Full&rdquo; dashboard tell you almost everything about the health of
your server:</p>
<ul>
<li><strong>CPU Busy.</strong> Short spikes are normal. It gets critical when the usage stays <em>permanently</em>
high – then the server is the bottleneck.</li>
<li><strong>Sys Load.</strong> The load should on average be below the number of your CPU cores (in the test:
4 cores → load permanently above 4 is a warning sign of overload).</li>
<li><strong>RAM Used.</strong> Important: Linux uses free memory as a file cache – so &ldquo;used&rdquo; isn&rsquo;t the same as
&ldquo;tight&rdquo;. Only when little stays free <em>besides</em> the cache and <strong>SWAP Used</strong> rises does the
server get sluggish. It&rsquo;s exactly this creeping rise you want to see early.</li>
<li><strong>Root FS Used.</strong> The classic among server outages is the full disk. The trend shows you the
fill rate – so you recognize days in advance when it&rsquo;s getting tight.</li>
<li><strong>Network Traffic.</strong> Unusual spikes even though nothing is running can indicate a backup, an
update or – in the bad case – unwanted traffic.</li>
</ul>
<p>In the cAdvisor dashboard the most exciting question is: <strong>Which container draws the load?</strong> If
your server suddenly gets slow, you see here at a glance whether it&rsquo;s due to a single app – and
don&rsquo;t have to guess. This turns &ldquo;something is slow&rdquo; into a targeted &ldquo;the photo import in
container X is eating the CPU right now&rdquo;.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>A target is at <code>up = 0</code> or &ldquo;DOWN&rdquo; in Prometheus.</strong> Prometheus can&rsquo;t reach the exporter. For
<code>cadvisor</code>, check that it&rsquo;s on the same <code>monitoring</code> network and the name and port are exactly
right (<code>cadvisor:8080</code>). For the <code>node</code> job, check the <code>extra_hosts</code> entry on the Prometheus
service and the target <code>host.docker.internal:9100</code> – without both, Prometheus doesn&rsquo;t find the
node-exporter in the host network. After changes to the <code>prometheus.yml</code>, restart the
Prometheus container: <code>docker compose restart prometheus</code>.</p>
<p><strong>The cAdvisor dashboard partly shows &ldquo;No data&rdquo; or cAdvisor won&rsquo;t start.</strong> If the mounts or
<code>privileged: true</code> are missing, cAdvisor doesn&rsquo;t see the containers. Check the <code>cadvisor</code> block
(mounts, <code>devices: /dev/kmsg</code>). Individual &ldquo;No data&rdquo; panels are normal – some metrics (like CPU
throttling) only exist if you&rsquo;ve set CPU limits on the containers.</p>
<p><strong>Grafana doesn&rsquo;t load correctly behind Traefik – login fails or the layout is broken.</strong> Almost
always <code>GF_SERVER_ROOT_URL</code> is wrong. It must be exactly the public address
(<code>https://grafana.YOUR_DOMAIN</code>). Also check that the Traefik label
<code>loadbalancer.server.port=3000</code> is set – Grafana listens on 3000 internally.</p>
<p><strong>Grafana shows &ldquo;No data&rdquo; in the panels, even though the targets are <code>up</code>.</strong> Usually the time
range. A fresh stack has no history yet – set &ldquo;Last 15 minutes&rdquo; at the top right and wait a few
minutes for data to accumulate. Also check that the <strong>Prometheus</strong> data source was selected on
dashboard import.</p>
<p><strong>The disk space on the server grows steadily.</strong> That&rsquo;s the Prometheus TSDB. Reduce
<code>--storage.tsdb.retention.time</code> (e.g. to <code>15d</code>) or monitor fewer targets. The <code>prom_data</code>
volume grows with the number of metrics × retention time.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>What you must back up.</strong> Important is the <strong><code>grafana_data</code> volume</strong>: it contains your users,
settings and self-built dashboards. Back it up encrypted and off-site with
<a href="/en/tutorials/restic-backups/">Restic</a>. The Prometheus metrics (<code>prom_data</code>) are pure
measurements – a loss is bearable, a backup optional.</li>
<li><strong>Updates.</strong> The image tags are pinned (<code>prometheus:v3.13.1</code>, <code>grafana:13.1.0</code> …). To update,
raise the tags, then <code>docker compose pull &amp;&amp; docker compose up -d</code>. Read the release notes
first – Grafana in particular occasionally changes defaults between major versions. You get
cAdvisor from <code>gcr.io</code>; there <code>v0.55.1</code> is currently the latest available image version.</li>
<li><strong>Security.</strong> Grafana is publicly accessible – assign a strong admin password and leave
<code>GF_USERS_ALLOW_SIGN_UP</code> at <code>false</code>. Prometheus and cAdvisor stay internal; don&rsquo;t expose them
&ldquo;just quickly&rdquo; for debugging, they have no authentication.</li>
<li><strong>The next step: alerts.</strong> A dashboard only helps if you look at it. Grafana can trigger
alerts itself at thresholds (RAM &gt; 90%, disk almost full) and report e.g. via push. This turns
the pretty dashboard into an early-warning system – a good connection to your existing
<a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a>.</li>
<li><strong>Honest about the effort:</strong> the stack runs largely on its own after setup. Occasionally plan
a look at the retention time and storage usage, and bump the versions roughly quarterly.</li>
</ul>
]]></content:encoded></item><item><title>Installing Uptime Kuma: server monitoring behind Traefik</title><link>https://serverkueche.de/en/tutorials/uptime-kuma-monitoring/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/uptime-kuma-monitoring/</guid><description>Set up Uptime Kuma behind Traefik and monitor your services: monitors, notifications and a status page – the first real app of the series.</description><content:encoded><![CDATA[<p>Traefik is up – now we hang the first real app behind it. <strong>Uptime Kuma</strong> is ideal for
that: tiny, immediately useful and without a database. It monitors your services and raises
the alarm when one fails – from now on <em>you</em> notice first that something is stuck, not your
users. Along the way you learn the pattern every further app repeats.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>Uptime Kuma 2.4</strong> runs at <code>https://status.YOUR_DOMAIN</code>, secured via Traefik
with automatic HTTPS. You&rsquo;ve set up the first monitor (which checks every minute whether a
service responds), connected a notification and optionally a public status page. For the
first time we bind a service that <strong>doesn&rsquo;t listen on port 80</strong> – an important detail for all
the following apps.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A running <a href="/en/tutorials/traefik-reverse-proxy/">Traefik reverse proxy</a> including the
<code>proxy</code> network and a working Let&rsquo;s Encrypt resolver <code>le</code></li>
<li>A DNS record <code>status.YOUR_DOMAIN</code> that
<a href="/en/tutorials/connect-domain-to-server/">points to the server</a></li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-the-composeyaml">Step 1: The compose.yaml</h3>
<p>Own folder, own file:</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 ~/uptime-kuma <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/uptime-kuma</span></span></code></pre></div>
</div>
<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">uptime-kuma</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">louislam/uptime-kuma:2</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">kuma-data:/app/data</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></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">proxy</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.kuma.rule=Host(`status.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.kuma.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.kuma.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.kuma.loadbalancer.server.port=3001&#34;</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="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">kuma-data</span><span class="p">:</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></span></code></pre></div>
</div>
<p>You know this from the Traefik tutorial – except for <strong>one new, decisive line</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="w">      </span>- <span class="s2">&#34;traefik.http.services.kuma.loadbalancer.server.port=3001&#34;</span></span></span></code></pre></div>
</div>
<p>Uptime Kuma listens internally on <strong>port 3001</strong>, not on 80. This line tells Traefik which
port to forward the requests to. Without it, Traefik guesses wrong and you get a <code>Bad Gateway</code>. Remember the label – every app that doesn&rsquo;t run on port 80 needs it.</p>
<p>The rest is the familiar pattern: <strong>no <code>ports:</code></strong> (only reachable via Traefik), <code>proxy</code>
network, named volume for the data.</p>
<h3 id="step-2-start-and-first-login">Step 2: Start and first login</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><span class="line"><span class="cl">docker compose logs -f uptime-kuma</span></span></code></pre></div>
</div>
<p>When Kuma is ready, the log ends with this line – from here the service accepts requests
(<code>Ctrl+C</code> only ends the following):</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">[SERVER] INFO: Welcome to Uptime Kuma
</span></span><span class="line"><span class="cl">[SERVER] INFO: Uptime Kuma Version: 2.4.0
</span></span><span class="line"><span class="cl">[SETUP-DATABASE] INFO: Listening on:
</span></span><span class="line"><span class="cl">[SETUP-DATABASE] INFO: -  http://localhost:3001</span></span></code></pre></div>
</div>
<p>On the first start, Kuma creates its database in the volume – that takes a few seconds. Then
open <code>https://status.YOUR_DOMAIN</code> in the browser. Uptime Kuma 2.x first asks for the
<strong>database</strong> – for a setup like ours, <strong>SQLite</strong> is the right, simplest choice (select it,
click <strong>Next</strong>). Right after that you create the <strong>admin account</strong> (username + strong
password).</p>
<p><figure class="my-6"><img src="/en/tutorials/uptime-kuma-monitoring/kuma-setup_hu_1d0c56e67101b8af.webp" srcset="/en/tutorials/uptime-kuma-monitoring/kuma-setup_hu_8fae09095cd4616e.webp 480w, /en/tutorials/uptime-kuma-monitoring/kuma-setup_hu_1d0c56e67101b8af.webp 768w, /en/tutorials/uptime-kuma-monitoring/kuma-setup_hu_6992115843b8c5c4.webp 1200w, /en/tutorials/uptime-kuma-monitoring/kuma-setup_hu_7dac57d86ca48603.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/uptime-kuma-monitoring/kuma-setup_hu_2fd60a2da8963f4b.webp"
    alt="Uptime Kuma&rsquo;s initial setup: choose the language and create the admin account" title="Initial setup: choose the language and create the admin account"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Initial setup: choose the language and create the admin account</figcaption></figure></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>Create the admin account immediately
  </p>
  <div class="prose-kitchen text-sm">As long as no admin account exists, <strong>anyone</strong> who opens the page can create one. Do this
right after the first start – not &ldquo;later&rdquo;.</div>
</div>
<h3 id="step-3-create-the-first-monitor">Step 3: Create the first monitor</h3>
<p>A <strong>monitor</strong> is a recurring check. Click <strong>Add New Monitor</strong> and create one for Traefik
itself:</p>
<ul>
<li><strong>Monitor type:</strong> <code>HTTP(s)</code></li>
<li><strong>Friendly name:</strong> <code>Traefik Dashboard</code></li>
<li><strong>URL:</strong> <code>https://traefik.YOUR_DOMAIN</code></li>
<li><strong>Heartbeat interval:</strong> <code>60</code> seconds</li>
<li><strong>Authentication:</strong> the Traefik dashboard is protected by basic auth (step 7 of the
<a href="/en/tutorials/traefik-reverse-proxy/">Traefik tutorial</a>) – so choose <strong>HTTP Basic Auth</strong> as
the authentication method below and enter the user and password. Without credentials, Kuma
only gets <code>401</code> and reports the monitor as <strong>Down</strong>.</li>
</ul>
<p>Save – after a few seconds the monitor is <strong>Up</strong> (green) and shows the response time.</p>
<p><figure class="my-6"><img src="/en/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_ecad2a835a6b6005.webp" srcset="/en/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_b9fd222e62394661.webp 480w, /en/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_ecad2a835a6b6005.webp 768w, /en/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_84686854fd419462.webp 1200w, /en/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_25a9861864b2b7ef.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/uptime-kuma-monitoring/kuma-dashboard_hu_efc1a897fd354ecc.webp"
    alt="The Uptime Kuma dashboard with two running monitors – both green with 100% availability and &ldquo;200 – OK&rdquo;" title="The dashboard with two running monitors"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The dashboard with two running monitors</figcaption></figure></p>
<p>Besides the simple <code>HTTP(s)</code> check, it pays to choose the matching <strong>monitor type</strong>:</p>
<ul>
<li><strong>HTTP(s) – Keyword:</strong> additionally checks whether a certain word appears in the response
text. This detects &ldquo;the server responds, but shows an error page&rdquo;.</li>
<li><strong>TCP port:</strong> for services without a web interface (e.g. a database, an SSH port). Only
checks whether the port is open.</li>
<li><strong>Ping:</strong> the simplest reachability test via ICMP.</li>
<li><strong>Docker container:</strong> checks the container status directly via the Docker socket – handy
for internal services without their own domain.</li>
<li><strong>Push:</strong> here <em>the monitored service</em> calls Kuma regularly. Ideal for cron jobs and
backups: if the script doesn&rsquo;t report in time, Kuma raises the alarm (&ldquo;dead man&rsquo;s switch&rdquo;).</li>
</ul>
<p>Create the matching monitor for every important service.</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">Check <strong>public services via their real domain</strong> (<code>https://…</code>), not via <code>localhost</code>. This way
you simultaneously test that Traefik and the certificate work from outside – not just that
the container is running.</div>
</div>
<p>For the <code>HTTP(s)</code> monitor, the option <strong>&ldquo;certificate expiry notification&rdquo;</strong> is also worth it:
Kuma then warns in good time before a TLS certificate expires. For services behind Traefik,
Let&rsquo;s Encrypt renews automatically – but this very automation occasionally fails silently (a
DNS record is changed, port 80 accidentally closed). The monitor is your safety net and
reports in while there are still days to fix it, instead of visitors suddenly facing a
certificate warning.</p>
<h3 id="step-4-set-up-notifications-email-and-telegram">Step 4: Set up notifications (email and Telegram)</h3>
<p>A monitor without an alarm is just a pretty chart. Under <strong>Settings → Notifications → Setup
Notification</strong> you create a channel. Uptime Kuma supports over 90 – we set up the two most
common completely: <strong>email</strong> for the classic message and <strong>Telegram</strong> for push straight to
the phone.</p>
<h4 id="email-smtp">Email (SMTP)</h4>
<p>Choose &ldquo;Email (SMTP)&rdquo; as the <strong>notification service</strong> and enter your mail provider&rsquo;s data:</p>
<ul>
<li><strong>Hostname / port:</strong> e.g. <code>smtp.YOUR_PROVIDER.com</code> and <code>587</code></li>
<li><strong>Security:</strong> <code>STARTTLS</code> (port 587) or <code>TLS/SSL</code> (port 465)</li>
<li><strong>Username / password:</strong> your SMTP credentials</li>
<li><strong>From / to address:</strong> which address the warning comes from and which it&rsquo;s sent to</li>
</ul>
<p><figure class="my-6"><img src="/en/tutorials/uptime-kuma-monitoring/kuma-email_hu_7f1e9c91a901332a.webp" srcset="/en/tutorials/uptime-kuma-monitoring/kuma-email_hu_e7e333a89de51892.webp 480w, /en/tutorials/uptime-kuma-monitoring/kuma-email_hu_7f1e9c91a901332a.webp 768w, /en/tutorials/uptime-kuma-monitoring/kuma-email_hu_a1d62b8025eec0b7.webp 1200w, /en/tutorials/uptime-kuma-monitoring/kuma-email_hu_6c1a492676432907.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/uptime-kuma-monitoring/kuma-email_hu_dc87d46846cc61b7.webp"
    alt="The email notification (SMTP) in Uptime Kuma: hostname, port, security and from address" title="Setting up the email notification (SMTP)"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Setting up the email notification (SMTP)</figcaption></figure></p>
<p>Click <strong>Test</strong> – within a few seconds a test message lands in the mailbox. Only when it
really arrives are the credentials and port correct. Don&rsquo;t forget to save.</p>
<h4 id="telegram">Telegram</h4>
<p>Telegram is ideal for instant push alerts to the phone – without your own mail server. You
need two pieces of information:</p>
<ol>
<li><strong>Bot token:</strong> message <a href="https://t.me/BotFather">@BotFather</a> in Telegram, send <code>/newbot</code>,
assign a name – BotFather replies with the <strong>token</strong>.</li>
<li><strong>Chat ID:</strong> send your new bot any message, then open
<code>https://api.telegram.org/bot&lt;YOUR_TOKEN&gt;/getUpdates</code> in the browser and read the
<code>chat.id</code> from the response.</li>
</ol>
<p>Enter the token and chat ID into the Telegram notification (Uptime Kuma links both helpers
directly in the dialog) and click <strong>Test</strong> – the message should appear in the chat
immediately.</p>
<p><figure class="my-6"><img src="/en/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_1feb4facbe88ee6e.webp" srcset="/en/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_c4bb1dc005fd5de9.webp 480w, /en/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_1feb4facbe88ee6e.webp 768w, /en/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_f338e7e25e86656.webp 1200w, /en/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_fa7e43c2efbd8e16.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/uptime-kuma-monitoring/kuma-telegram_hu_2c05a870636172bd.webp"
    alt="The Telegram notification in Uptime Kuma: bot token and chat ID, with a direct link to the BotFather" title="Telegram notification: bot token and chat ID"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Telegram notification: bot token and chat ID</figcaption></figure></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>Otherwise the alarm stays silent
  </p>
  <div class="prose-kitchen text-sm">A configured notification does <strong>not</strong> take effect automatically. Enable it in each monitor
(checkbox in the monitor form) or, in the notification dialog, turn on &ldquo;Enabled by default&rdquo;
and &ldquo;Apply on all existing monitors&rdquo;.</div>
</div>
<p>If you want push messages entirely under your own control (without a Telegram server),
<strong>ntfy</strong> comes later – self-hosted, with its own recipe in the series.</p>
<h3 id="step-5-a-public-status-page-optional">Step 5: A public status page (optional)</h3>
<p>Kuma can publish a <strong>status page</strong> where visitors see whether your services are running.
Here&rsquo;s how:</p>
<ol>
<li><strong>Status Pages → New Status Page</strong>, then assign a <strong>name</strong> (e.g. &ldquo;Serverküche Status&rdquo;) and
a <strong>slug</strong> (the URL, e.g. <code>serverkueche</code>) and click <strong>Next</strong>.</li>
<li>In the editor, <strong>Add Group</strong> (e.g. &ldquo;Services&rdquo;), and below it, add the desired <strong>monitors</strong>
via the selection field.</li>
<li><strong>Save</strong> at the top right – done. The page is then publicly reachable at
<code>https://status.YOUR_DOMAIN/status/serverkueche</code>.</li>
</ol>
<p>Only include what really everyone may see – better leave internal services out.</p>
<p><figure class="my-6"><img src="/en/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_c4b45400765e134.webp" srcset="/en/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_34f9e0e0768594d.webp 480w, /en/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_c4b45400765e134.webp 768w, /en/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_b036ea3223a2da7.webp 1200w, /en/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_3c0dc17506dd64f2.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/uptime-kuma-monitoring/kuma-statusseite_hu_652cbd6c4d4b33a1.webp"
    alt="The public status page &ldquo;Serverküche Status&rdquo;: &ldquo;All Systems Operational&rdquo; and both services with 100% availability" title="The finished public status page"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The finished public status page</figcaption></figure></p>
<h3 id="step-6-avoid-false-alarms--fine-tune-the-alarm-behavior">Step 6: Avoid false alarms – fine-tune the alarm behavior</h3>
<p>A monitor that raises the alarm at every brief network hiccup is quickly ignored – and then
you miss the real outage. In the monitor form you set the behavior appropriately:</p>
<ul>
<li><strong>Retries:</strong> the service only counts as &ldquo;Down&rdquo; after <em>n</em> failed checks. <code>2</code>–<code>3</code> filters out
individual dropouts without obscuring real outages for long.</li>
<li><strong>Heartbeat interval on failure:</strong> Kuma may check more often in the error case (e.g. every
20 seconds) to detect recovery quickly.</li>
<li><strong>Resend notification:</strong> Kuma can remind you every <em>x</em> minutes as long as a service is down
– useful so an outage at night doesn&rsquo;t get lost in a single mail.</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>Plan maintenance windows
  </p>
  <div class="prose-kitchen text-sm">If you plan an update with downtime, create a <strong>maintenance window</strong> under <strong>Maintenance</strong>.
Kuma then pauses the alarms for the affected monitors – so you (and the status page) don&rsquo;t
get false alarms while you&rsquo;re at work.</div>
</div>
<h3 id="step-7-monitor-cron-jobs-with-a-push-monitor">Step 7: Monitor cron jobs with a push monitor</h3>
<p>Classic monitors check from outside whether a service <em>responds</em>. For things that <strong>run
silently in the background</strong> – a nightly backup, a sync script, a cron job – the <strong>push
monitor</strong> flips the principle: not Kuma asks, but <em>your script reports in</em>. If the report
fails to come, Kuma raises the alarm – the classic &ldquo;dead man&rsquo;s switch&rdquo;.</p>
<p>Create a monitor of type <strong>Push</strong>. Kuma then shows you a unique <strong>push URL</strong>:</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">https://status.YOUR_DOMAIN/api/push/YOUR_TOKEN?status=up&amp;msg=OK&amp;ping=</span></span></code></pre></div>
</div>
<p>You call this URL at the end of your script – for example after a successfully completed
backup:</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="c1"># ... your backup command ...</span>
</span></span><span class="line"><span class="cl">curl -fsS <span class="s2">&#34;https://status.YOUR_DOMAIN/api/push/YOUR_TOKEN?status=up&amp;msg=Backup+OK&#34;</span> &gt; /dev/null</span></span></code></pre></div>
</div>
<p>Set the <strong>check interval</strong> in Kuma a bit more generously than your cron cadence (if the
backup runs hourly, give Kuma e.g. 90 minutes of tolerance). If no <code>curl</code> comes in that time,
the monitor goes to <strong>Down</strong> and you&rsquo;re notified – so you learn about a backup that did <em>not</em>
run, not only when you urgently need it.</p>
<h3 id="step-8-secure-the-admin-login-with-2fa">Step 8: Secure the admin login with 2FA</h3>
<p>Your Kuma login protects access to all monitors, the stored notification credentials and the
status-page configuration – and it&rsquo;s publicly on the net. So enable <strong>two-factor
authentication</strong>: under <strong>Settings → Security → Two-Factor Authentication</strong>. Kuma shows a QR
code you scan with an authenticator app (e.g. Aegis or 2FAS); to activate, you enter the
generated code once.</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>Secure the recovery
  </p>
  <div class="prose-kitchen text-sm">Keep the TOTP secret or a second authenticator in a safe place (password manager). If you lose
your phone <strong>and</strong> have no copy, you can otherwise only get back in via the database in the
<code>kuma-data</code> volume.</div>
</div>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong><code>Bad Gateway</code> (502) when opening <code>status.YOUR_DOMAIN</code>.</strong> Almost always the port label
<code>traefik.http.services.kuma.loadbalancer.server.port=3001</code> is missing or has a wrong port.
Traefik then reaches the container but knocks on the wrong port. Check the label and run
<code>docker compose up -d</code> again.</p>
<p><strong><code>404 page not found</code> instead of Kuma.</strong> As with every app behind Traefik:
<code>traefik.enable=true</code> set? Container on the <code>proxy</code> network? Is the domain in the <code>Host(...)</code>
rule correct and does the DNS record <code>status.YOUR_DOMAIN</code> point to the server? The Traefik
dashboard shows under &ldquo;HTTP Routers&rdquo; whether <code>kuma</code> is registered.</p>
<p><strong>The interface loads, but the live update stutters / breaks off.</strong> Kuma uses WebSockets.
Traefik forwards those correctly by default – if the problem still occurs, it&rsquo;s usually an
upstream CDN/proxy (e.g. Cloudflare in &ldquo;proxy&rdquo; mode) that blocks WebSockets. For direct
operation behind Traefik, no extra configuration is needed.</p>
<p><strong>After a re-setup all monitors are gone.</strong> The <code>kuma-data</code> volume was deleted (e.g. by
<code>docker compose down -v</code>). All configuration and history lives solely in this volume – that&rsquo;s
why it&rsquo;s at the top of the next section.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Back up:</strong> the complete heart of Kuma is the <code>kuma-data</code> volume (a SQLite database). Back
it up regularly – if it&rsquo;s gone, all monitors and the history are gone. We build the off-site
backup for it in the <a href="/en/tutorials/restic-backups/">Restic tutorial</a>.</li>
<li><strong>Updates:</strong> the tag <code>:2</code> stays on the 2.x series and brings bug fixes with
<code>docker compose pull &amp;&amp; docker compose up -d</code>. Before a jump to a new major version (e.g.
later <code>:3</code>), read the release notes and back up the volume first.</li>
<li><strong>Coming from 1.x?</strong> The switch to <code>:2</code> <strong>migrates the SQLite database automatically on the
first start</strong> – that can take a moment, and going back to <code>:1</code> is not intended afterwards.
So back up the <code>kuma-data</code> volume <strong>beforehand</strong>, then you&rsquo;re on the safe side. New
installations (like above) aren&rsquo;t affected.</li>
<li><strong>Honest limitation:</strong> a monitor that runs <strong>on the same server</strong> as the monitored services
can&rsquo;t warn you when the whole server fails – then Kuma is offline too. For that case, add an
<strong>external</strong> watcher. Two cheap ways: a <strong>second Uptime Kuma</strong> on a small server (or at
home) that only monitors this instance via HTTP – or a <strong>free external ping service</strong> that
pings your public status page. This way you also get a notice when the whole host is gone –
the only case a local monitor inherently can&rsquo;t cover.</li>
</ul>
<p>With that you&rsquo;ve internalized the app pattern and monitor everything you hang behind Traefik
from now on. What every further app requires are
<a href="/en/tutorials/restic-backups/">encrypted off-site backups with Restic</a> – so your data
survives a server crash.</p>
]]></content:encoded></item></channel></rss>