<?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>Cron – Serverküche</title><link>https://serverkueche.de/en/tags/cron/</link><description>Cron – 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, 15 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/cron/index.xml" rel="self" type="application/rss+xml"/><item><title>Healthchecks: Monitor Cron Jobs &amp; Backups (Dead Man's Switch)</title><link>https://serverkueche.de/en/tutorials/healthchecks-monitor-jobs/</link><pubDate>Tue, 15 Sep 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/healthchecks-monitor-jobs/</guid><description>Notices when a backup does NOT run: Healthchecks monitors cron jobs via a dead man's switch and alerts on failure – self-hosted behind Traefik.</description><content:encoded><![CDATA[<p>The most dangerous failures are the silent ones: the nightly backup hasn&rsquo;t run for three weeks – and no one notices until the data is actually needed. <a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a> tells you when a service is <em>up</em>. Healthchecks tells you when a job <em>didn&rsquo;t run</em>. That difference is what saves data.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>A self-hosted <strong>Healthchecks</strong> server (v4.4) behind <a href="/en/tutorials/traefik-reverse-proxy/">Traefik</a> that works as a <strong>dead man&rsquo;s switch</strong>: every monitored job &ldquo;checks in&rdquo; after a successful run with a short HTTP ping. If that ping is missing (because the job crashed, the server was off, or the cron entry is gone), Healthchecks raises an alert. By the end you monitor your <a href="/en/tutorials/restic-backups/">Restic backups</a>, database dumps and any other recurring job with it – and get notified <em>before</em> the absence becomes a problem.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server with <a href="/en/tutorials/traefik-reverse-proxy/">Traefik</a> running and <a href="/en/tutorials/docker-compose-basics/">Docker Compose</a></li>
<li>A <a href="/en/tutorials/connect-domain-to-server/">subdomain pointing at the server</a> – <code>YOUR_DOMAIN</code> below</li>
<li>Recurring jobs worth monitoring (e.g. your <a href="/en/tutorials/restic-backups/">Restic backups</a>)</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/healthchecks-monitor-jobs/" data-content-piece="VPS 1000 G12.5">
  <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.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €14.50/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Healthchecks is frugal and runs happily alongside your other stack.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12.5-iv-24m-eu?ref=44083#vps-1000-g12.5-iv-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12.5 · {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"><div class="not-prose my-3 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">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1">
    <span>💶 <strong>€5 voucher</strong> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">(not for domains or VPS Lite)</span>
  </p>
  <p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
    <a href="https://www.netcup.com/en/checkout/cart?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|€5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Redeem in the cart →</a>
  </p>
</div></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-understand-the-principle--monitoring-inverted">Step 1: Understand the principle – monitoring, inverted</h3>
<p>Classic monitoring actively asks: &ldquo;Does the service respond?&rdquo; Healthchecks inverts that: the <strong>job</strong> checks in with the server. Each check has a unique <strong>ping URL</strong>. After a successful run the job calls that URL. Healthchecks expects the ping within a defined window (<strong>period</strong>) plus a tolerance (<strong>grace time</strong>). If the ping doesn&rsquo;t arrive in time, the check goes &ldquo;down&rdquo; and Healthchecks alerts. That&rsquo;s the <strong>dead man&rsquo;s switch</strong>: it&rsquo;s not the presence of a signal that triggers the alarm, but its absence.</p>
<h3 id="step-2-create-the-compose-file">Step 2: Create the Compose file</h3>
<p>Healthchecks is a Django application; we run it with SQLite – perfectly sufficient for a typical self-hosting scale. Create the project:</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/healthchecks/data <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/healthchecks
</span></span><span class="line"><span class="cl">chown -R 1000:1000 data</span></span></code></pre></div>
</div>
<p>The <code>compose.yaml</code> – replace <code>YOUR_DOMAIN</code> and generate your own <code>SECRET_KEY</code> (<code>openssl rand -hex 32</code>):</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">healthchecks</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">healthchecks/healthchecks:v4.4</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">user</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1000:1000&#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">./data:/data</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">SITE_ROOT</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">SITE_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">Serverkueche Healthchecks</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ALLOWED_HOSTS</span><span class="p">:</span><span class="w"> </span><span class="l">YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CSRF_TRUSTED_ORIGINS</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">DEBUG</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">DB</span><span class="p">:</span><span class="w"> </span><span class="l">sqlite</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DB_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">/data/hc.sqlite</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SECRET_KEY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;YOUR_RANDOM_KEY&#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="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.hc.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.hc.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.hc.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.hc.loadbalancer.server.port=8000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></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>Two traps: DB_NAME and no non-ASCII in SITE_NAME
  </p>
  <div class="prose-kitchen text-sm">With SQLite, <strong><code>DB_NAME: /data/hc.sqlite</code> is mandatory</strong> – without the path Healthchecks creates the database in a non-writable location and fails to start with &ldquo;unable to open database file&rdquo;. And: keep <strong><code>SITE_NAME</code> pure ASCII</strong> (no &ldquo;ü&rdquo;, &ldquo;ö&rdquo; …). A non-ASCII character in this environment variable causes a <code>UnicodeEncodeError: surrogates not allowed</code> on current Python – the page then responds with HTTP 500 (this exact bug hit me while testing this tutorial).</div>
</div>
<h3 id="step-3-start-and-log-in">Step 3: Start and log in</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div>
</div>
<p>The image ships a health check; Traefik only routes once the container is <code>healthy</code> (about 20–30 seconds). Check:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME                          IMAGE                        STATUS
</span></span><span class="line"><span class="cl">healthchecks-healthchecks-1   healthchecks/healthchecks:v4.4  Up (healthy)</span></span></code></pre></div>
</div>
<p>Healthchecks does <strong>not</strong> ship an account – on start the image only sets up the database. So
create your access once yourself (replace the password):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> healthchecks python manage.py createsuperuser <span class="se">\
</span></span></span><span class="line"><span class="cl">  --email admin@YOUR_DOMAIN --password YOUR_STRONG_PASSWORD</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Superuser created successfully.</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>SUPERUSER_EMAIL has no effect
  </p>
  <div class="prose-kitchen text-sm">Many guides list <code>SUPERUSER_EMAIL</code> and <code>SUPERUSER_PASSWORD</code> as environment variables in the
Compose file. The official image <strong>does not read them</strong> – its startup hook only runs
<code>manage.py migrate</code>. Anyone relying on that ends up at a login page with no account behind it
(tested with v4.3 <strong>and</strong> v4.4). The <code>createsuperuser</code> route above is the reliable one.</div>
</div>
<p>Now open <code>https://YOUR_DOMAIN/</code> and log in with those credentials:</p>
<p><figure class="my-6"><img src="/en/tutorials/healthchecks-monitor-jobs/hc-login_hu_b70ec50f1376f78d.webp" srcset="/en/tutorials/healthchecks-monitor-jobs/hc-login_hu_50c18b3066470d62.webp 480w, /en/tutorials/healthchecks-monitor-jobs/hc-login_hu_b70ec50f1376f78d.webp 768w, /en/tutorials/healthchecks-monitor-jobs/hc-login_hu_b57cdf37ed1dbbfd.webp 1200w, /en/tutorials/healthchecks-monitor-jobs/hc-login_hu_acedcfe6d6568f0b.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/healthchecks-monitor-jobs/hc-login_hu_68b55ee4cd86bb76.webp"
    alt="The Healthchecks login page under your own HTTPS domain" title="The self-hosted Healthchecks login – the account comes from the createsuperuser call"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The self-hosted Healthchecks login – the account comes from the createsuperuser call</figcaption></figure></p>
<h3 id="step-4-create-a-project-and-your-first-check">Step 4: Create a project and your first check</h3>
<p>After login create a project via <strong>New Project…</strong> (e.g. &ldquo;Serverküche&rdquo;) and inside it a first check via <strong>Add Check</strong>. Give it a descriptive name, tags and a schedule – <strong>period</strong> = expected interval between two runs (for a daily backup: 1 day), <strong>grace time</strong> = how long Healthchecks waits after the due time before alerting (e.g. 1 hour). The overview shows all checks with status, ping URL and last ping:</p>
<p><figure class="my-6"><img src="/en/tutorials/healthchecks-monitor-jobs/hc-checks_hu_d32b5bde7768acf2.webp" srcset="/en/tutorials/healthchecks-monitor-jobs/hc-checks_hu_afe3b1bc56c24cd4.webp 480w, /en/tutorials/healthchecks-monitor-jobs/hc-checks_hu_d32b5bde7768acf2.webp 768w, /en/tutorials/healthchecks-monitor-jobs/hc-checks_hu_dbc104bd7adacb8b.webp 1200w, /en/tutorials/healthchecks-monitor-jobs/hc-checks_hu_bfc1fb2a3b9fcc73.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/healthchecks-monitor-jobs/hc-checks_hu_7b886e8381e7fe59.webp"
    alt="The Healthchecks overview with several checks, ping URLs and status indicator" title="The check overview: a green tick for \&#34;up\&#34;, plus ping URL, schedule and last ping"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The check overview: a green tick for \&#34;up\&#34;, plus ping URL, schedule and last ping</figcaption></figure></p>
<p>Each check gets its own <strong>ping URL</strong> of the form <code>https://YOUR_DOMAIN/ping/&lt;UUID&gt;</code>. Clicking a check opens the detail page with instructions, history and status:</p>
<p><figure class="my-6"><img src="/en/tutorials/healthchecks-monitor-jobs/hc-detail_hu_e13246aedbbf057b.webp" srcset="/en/tutorials/healthchecks-monitor-jobs/hc-detail_hu_2424adeceaa1d5b2.webp 480w, /en/tutorials/healthchecks-monitor-jobs/hc-detail_hu_e13246aedbbf057b.webp 768w, /en/tutorials/healthchecks-monitor-jobs/hc-detail_hu_e8a6e7129b4b2ea4.webp 1200w, /en/tutorials/healthchecks-monitor-jobs/hc-detail_hu_aed8185972eebfb.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/healthchecks-monitor-jobs/hc-detail_hu_50e0ad1aab93dcf6.webp"
    alt="The detail page of a check with ping URL, current &quot;up&quot; status and event log" title="The detail page: ping URL, current status and the log of received pings"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The detail page: ping URL, current status and the log of received pings</figcaption></figure></p>
<h3 id="step-5-have-a-job-send-the-ping">Step 5: Have a job send the ping</h3>
<p>Now the core. You make your job call the ping URL after a successful run. The simplest example – at the end of your script:</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 -fsS -m <span class="m">10</span> --retry <span class="m">5</span> https://YOUR_DOMAIN/ping/YOUR_CHECK_UUID</span></span></code></pre></div>
</div>
<p><code>-fsS</code> keeps curl quiet but reports errors; <code>-m 10</code> aborts after 10 seconds; <code>--retry 5</code> catches brief network hiccups. Even better: report the job&rsquo;s <strong>exit code</strong>, so a <em>failed</em> run shows up as an error immediately instead of &ldquo;no ping&rdquo;:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nv">URL</span><span class="o">=</span><span class="s2">&#34;https://YOUR_DOMAIN/ping/YOUR_CHECK_UUID&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ... your actual job runs here ...</span>
</span></span><span class="line"><span class="cl">restic backup /important/data
</span></span><span class="line"><span class="cl"><span class="c1"># report the exit code to Healthchecks (0 = ok, otherwise failure)</span>
</span></span><span class="line"><span class="cl">curl -fsS -m <span class="m">10</span> --retry <span class="m">5</span> <span class="s2">&#34;</span><span class="nv">$URL</span><span class="s2">/</span><span class="nv">$?</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<h3 id="step-6-monitor-restic-backups">Step 6: Monitor Restic backups</h3>
<p>This is the showcase. If your <a href="/en/tutorials/restic-backups/">Restic backups</a> run via a <a href="/en/tutorials/systemd-basics/">systemd timer</a>, you add the ping at the end of the backup script. If the backup doesn&rsquo;t run (timer disabled, server off, script crashed), the ping is missing – and after the grace time expires, Healthchecks alerts. That way you learn about a dead backup within hours, not at data-loss time.</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>Use start and failure signals
  </p>
  <div class="prose-kitchen text-sm">Healthchecks can do more than &ldquo;done&rdquo;: a ping to <code>.../ping/UUID/start</code> <strong>before</strong> the job additionally measures the run time, a ping to <code>.../ping/UUID/fail</code> actively reports a failure. So you see not just <em>whether</em> but also <em>how long</em> a job ran – useful for spotting backups that are getting slower over time.</div>
</div>
<h3 id="step-7-set-up-notifications">Step 7: Set up notifications</h3>
<p>An alert is only useful if it reaches you. Under <strong>Integrations</strong> you connect channels: email (set the SMTP environment variables for it), <a href="/en/tutorials/ntfy-push-notifications/">ntfy</a>, Telegram, webhooks and many more. For the self-hosting stack ntfy is the obvious choice – push to your phone, without a third-party service. Set up at least one channel and assign it to your checks, otherwise the &ldquo;down&rdquo; status stays silent.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>Container won&rsquo;t start: &ldquo;unable to open database file&rdquo;.</strong> <code>DB_NAME: /data/hc.sqlite</code> is missing or the <code>data</code> folder isn&rsquo;t owned by UID 1000. Set the path and <code>chown -R 1000:1000 /opt/healthchecks/data</code> (see the warning in step 2).</p>
<p><strong>The page responds with HTTP 500.</strong> Most common cause: a non-ASCII character in <code>SITE_NAME</code> (or another text environment variable). Switch to pure ASCII and restart. To diagnose, temporarily set <code>DEBUG: &quot;True&quot;</code> – but switch it back to <code>False</code> afterwards.</p>
<p><strong>Login fails / CSRF error on submit.</strong> <code>CSRF_TRUSTED_ORIGINS: https://YOUR_DOMAIN</code> must be set – Django otherwise rejects POST requests behind the reverse proxy. And <code>ALLOWED_HOSTS</code> must contain your domain exactly.</p>
<p><strong>The check won&rsquo;t turn &ldquo;green&rdquo; even though the job runs.</strong> Check whether the <code>curl</code> ping actually runs and hits the right UUID: <code>curl -v https://YOUR_DOMAIN/ping/UUID</code> should return <code>OK</code>. On the detail page the log shows whether and from which IP pings arrive.</p>
<p><strong>I get no notification on &ldquo;down&rdquo;.</strong> No integration channel is assigned, or (for email) the SMTP settings are missing. Set up a channel under &ldquo;Integrations&rdquo; and assign it to the check.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Updates.</strong> Occasionally bump the image tag (<code>healthchecks/healthchecks:v4.4</code>) to the current version and <code>docker compose up -d</code>; the database migrates automatically at start. Your normal <a href="/en/tutorials/keep-docker-stack-updated/">update process</a> handles the rest.</li>
<li><strong>Backup.</strong> The entire state lives in the SQLite file under <code>data/</code> – add it to your <a href="/en/tutorials/restic-backups/">Restic backup</a>. A small but neat side effect: Healthchecks then monitors the backup that backs it up – close the loop by having the backup job also ping a Healthchecks check.</li>
<li><strong>Who watches the watchman?</strong> Healthchecks itself has to be running to alert. So add it to <a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a> as an HTTP monitor – the two tools then cover each other: Kuma checks that Healthchecks is <em>reachable</em>, Healthchecks checks that your jobs <em>ran</em>.</li>
</ul>
]]></content:encoded></item></channel></rss>