<?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>Notifications – Serverküche</title><link>https://serverkueche.de/en/tags/notifications/</link><description>Notifications – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Fri, 07 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/notifications/index.xml" rel="self" type="application/rss+xml"/><item><title>ntfy: push notifications from your own server to your phone</title><link>https://serverkueche.de/en/tutorials/ntfy-push-notifications/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/ntfy-push-notifications/</guid><description>Self-host ntfy with Docker &amp; Traefik: push messages to your phone via a simple curl – ideal for backup reports, monitoring alerts and scripts.</description><content:encoded><![CDATA[<p>&ldquo;Backup failed&rdquo;, &ldquo;Disk almost full&rdquo;, &ldquo;new SSH login&rdquo; – you want messages like these on your phone immediately, without handing them to a third-party push service. With ntfy you build your own push server in a few minutes: a message is just a <code>curl</code> call away.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>ntfy 2.26</strong> runs in Docker behind your Traefik reverse proxy, reachable under your own (sub)domain with HTTPS. You can then send a push message from any script, cron job or monitoring tool – with a simple HTTP request – and get it displayed instantly in the ntfy app on your phone or in the browser, including title, priority, emojis and optional actions.</p>
<p>The clever part: ntfy is extremely lightweight (runs easily on the smallest VPS) and the data stays with you. We set the server up <strong>private</strong> right away – only with credentials may someone send or read messages.</p>
<p>Why ntfy and not a ready-made service like Pushover or a Telegram bot? Because here you need no account with third parties, hand no message contents to foreign servers, and the interface is as simple as it gets: anything that can fire an HTTP request – a shell script, a cron job, a monitoring tool – can notify you, without any SDK or API library. Exactly this reduction to &ldquo;POST to a URL&rdquo; makes ntfy so easy to connect to a self-hosted setup.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="/en/tutorials/harden-ssh/">hardened server</a> with Debian 13 and Docker</li>
<li>A <a href="/en/tutorials/traefik-reverse-proxy/">running Traefik reverse proxy</a> with the external network <code>proxy</code> and the resolver <code>le</code></li>
<li>A (sub)domain whose A/AAAA record <a href="/en/tutorials/connect-domain-to-server/">points to your server</a>, e.g. <code>ntfy.YOUR_DOMAIN</code></li>
<li>Optional but recommended: the <strong>ntfy app</strong> (Android/F-Droid/iOS) to really get push on your phone</li>
</ul>
<p>ntfy is so frugal that it runs without problems alongside many other services on a VPS 1000 – the <a href="/en/server-calculator/">server calculator</a> shows you how little it needs.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /en/tutorials/ntfy-push-notifications/" data-content-piece="VPS 1000 G12">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">More than enough for ntfy – the service needs hardly any resources.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

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

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-write-the-composeyaml">Step 1: Write the <code>compose.yaml</code></h3>
<p>Create the project folder:</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/ntfy <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/ntfy</span></span></code></pre></div>
</div>
<p>Create <code>/opt/ntfy/compose.yaml</code>. Replace <strong><code>ntfy.YOUR_DOMAIN</code> with your real (sub)domain</strong> – it appears both in the <code>NTFY_BASE_URL</code> and in the Traefik label:</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">ntfy</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">binwiederhier/ntfy:v2.26</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">ntfy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">serve</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">NTFY_BASE_URL</span><span class="p">:</span><span class="w"> </span><span class="l">https://ntfy.YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_LISTEN_HTTP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_BEHIND_PROXY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_ENABLE_LOGIN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_REQUIRE_LOGIN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_CACHE_FILE</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/ntfy/cache.db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_AUTH_FILE</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/ntfy/auth.db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_AUTH_DEFAULT_ACCESS</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;deny-all&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NTFY_ATTACHMENT_CACHE_DIR</span><span class="p">:</span><span class="w"> </span><span class="l">/var/lib/ntfy/attachments</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:/var/lib/ntfy</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">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.ntfy.rule=Host(`ntfy.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.ntfy.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.ntfy.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.ntfy.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></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="l">data:</span></span></span></code></pre></div>
</div>
<p>The most important lines:</p>
<ul>
<li><strong><code>NTFY_BASE_URL</code></strong> must be exactly your public HTTPS address – otherwise the app and the web client refuse the connection.</li>
<li><strong><code>NTFY_BEHIND_PROXY: &quot;true&quot;</code></strong> tells ntfy it runs behind Traefik, so it reads the real visitor IP from the proxy headers (important for rate limiting).</li>
<li><strong><code>NTFY_AUTH_DEFAULT_ACCESS: &quot;deny-all&quot;</code></strong> is the security screw: by default <strong>nobody</strong> may read or write. Access is only granted to whoever we explicitly create in a moment. Without this line your server would be an open push relay for the whole world.</li>
<li><strong><code>NTFY_ENABLE_LOGIN</code> / <code>NTFY_REQUIRE_LOGIN</code></strong> enable the login in the web client and make it mandatory. With <code>NTFY_REQUIRE_LOGIN</code>, an unauthenticated visitor gets a login screen directly instead of an empty interface.</li>
</ul>
<h3 id="step-2-start-the-server">Step 2: Start the server</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>Check that the container is 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>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME   IMAGE                       COMMAND         SERVICE   STATUS
</span></span><span class="line"><span class="cl">ntfy   binwiederhier/ntfy:v2.26    &#34;ntfy serve&#34;    ntfy      Up 4 seconds</span></span></code></pre></div>
</div>
<p>(For readability, the columns <code>CREATED</code> and <code>PORTS</code> are hidden here.)</p>
<p>Traefik fetches a certificate automatically. Check the health endpoint from your machine:</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 -s https://ntfy.YOUR_DOMAIN/v1/health</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">{&#34;healthy&#34;:true}</span></span></code></pre></div>
</div>
<p>If the server responds with <code>{&quot;healthy&quot;:true}</code>, the chain of DNS, Traefik, TLS and ntfy is in place.</p>
<h3 id="step-3-create-a-user">Step 3: Create a user</h3>
<p>Because we set <code>deny-all</code>, you need at least one user. Create an administrator (they may write to and read all topics):</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 <span class="nb">exec</span> -it ntfy ntfy user add --role<span class="o">=</span>admin admin</span></span></code></pre></div>
</div>
<p>ntfy asks twice for a password and then confirms:</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">user admin added with role admin</span></span></code></pre></div>
</div>
<p>Check the result:</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 <span class="nb">exec</span> ntfy ntfy user list</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">user admin (role: admin, tier: none)
</span></span><span class="line"><span class="cl">- read-write access to all topics (admin role)</span></span></code></pre></div>
</div>
<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">For individual people or devices that should only use certain topics, you create normal users (<code>--role=user</code>) and grant rights specifically. Create the user first (<code>docker exec -it ntfy ntfy user add --role=user phone</code>) and then assign it the right, e.g. <code>docker exec ntfy ntfy access phone &quot;serverkueche-alerts&quot; read-only</code>. This way the phone may read along but not send.</div>
</div>
<h3 id="step-4-subscribe-to-a-topic">Step 4: Subscribe to a topic</h3>
<p>In ntfy everything runs via <strong>topics</strong> – freely chosen names under which messages run. Open <code>https://ntfy.YOUR_DOMAIN</code> in the browser. Thanks to <code>NTFY_REQUIRE_LOGIN</code>, the web client greets you directly with a <strong>login screen</strong> – log in there with your created <code>admin</code> account (without login, <code>deny-all</code> blocks every read and subscribe attempt with <code>HTTP 403</code>). Then click <strong>Subscribe to topic</strong> on the left and choose a <strong>hard-to-guess name</strong> (e.g. <code>serverkueche-alerts</code>):</p>
<p><figure class="my-6"><img src="/en/tutorials/ntfy-push-notifications/ntfy-abonnieren_hu_4253a49394cd264a.webp" srcset="/en/tutorials/ntfy-push-notifications/ntfy-abonnieren_hu_bfb2e580655bae5.webp 480w, /en/tutorials/ntfy-push-notifications/ntfy-abonnieren_hu_4253a49394cd264a.webp 768w, /en/tutorials/ntfy-push-notifications/ntfy-abonnieren_hu_59ece808b740a56d.webp 1200w, /en/tutorials/ntfy-push-notifications/ntfy-abonnieren_hu_a306283b64efaad9.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/ntfy-push-notifications/ntfy-abonnieren_hu_1cc81d2cbc6e51c8.webp"
    alt="ntfy web interface with the dialog to subscribe to a new topic" title="Subscribe to a topic – the name should be hard to guess"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Subscribe to a topic – the name should be hard to guess</figcaption></figure></p>
<p>On the phone you set up your own server once in the ntfy app: open <strong>Settings → Default server</strong> and enter <code>https://ntfy.YOUR_DOMAIN</code>. Then store your credentials for this server under <strong>User management</strong>. Now you can add a topic just like in the browser – the app no longer asks about the big ntfy.sh server, but uses yours. From now on every message to this topic lands as a real push notification on the device, even with the app closed. On iOS the reception runs via Apple&rsquo;s push service; the app explains at first launch what&rsquo;s needed once for that.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Allow browser notifications
  </p>
  <div class="prose-kitchen text-sm">If the web client shows &ldquo;notifications are blocked&rdquo; at the top left, you still have to allow them in your browser for this site – otherwise you only see the messages while the tab is open. For reliable push messages on the go, the phone app is the better choice anyway.</div>
</div>
<h3 id="step-5-send-a-message">Step 5: Send a message</h3>
<p>Now the actual purpose – send a message via <code>curl</code>. Because the server is private, you supply your credentials (<code>-u USER:PASSWORD</code>):</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 -u admin:YOUR_PASSWORD -d <span class="s2">&#34;Backup completed successfully&#34;</span> https://ntfy.YOUR_DOMAIN/serverkueche-alerts</span></span></code></pre></div>
</div>
<p>That&rsquo;s it – the message is instantly on all subscribed devices. With a few additional headers it becomes more meaningful: title, priority (1–5) and tags (which also appear as emojis):</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 -u admin:YOUR_PASSWORD <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Title: Monitoring warning&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Priority: 4&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Tags: warning&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s2">&#34;High CPU load on the VPS&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://ntfy.YOUR_DOMAIN/serverkueche-alerts</span></span></code></pre></div>
</div>
<p>The <strong>priority</strong> controls how intrusive the message arrives: <code>5</code> (max) makes the phone ring and vibrate, <code>4</code> (high) reports clearly, <code>3</code> is the default, <code>2</code> and <code>1</code> (low/min) land quietly in the list. For &ldquo;server unreachable&rdquo; you use 5, for &ldquo;backup ok&rdquo; rather 2 – this way you don&rsquo;t become numb to the important alarms.</p>
<p><strong>Tags</strong> turn into emojis if they match a known name: <code>warning</code> → ⚠️, <code>white_check_mark</code> → ✅, <code>rotating_light</code> → 🚨. Multiple tags you give comma-separated (<code>-H &quot;Tags: rotating_light,skull&quot;</code>). And with the <code>Click</code> header, a tap on the message opens a URL directly – handy to jump straight from the alarm into the dashboard:</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 -u admin:YOUR_PASSWORD <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Title: Uptime Kuma: service down&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Priority: 5&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Tags: rotating_light&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Click: https://status.YOUR_DOMAIN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s2">&#34;Nextcloud is not responding&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://ntfy.YOUR_DOMAIN/serverkueche-alerts</span></span></code></pre></div>
</div>
<p>It gets really powerful with <strong>action buttons</strong>: via the <code>Actions</code> header you show buttons right in the notification that open a URL or trigger an HTTP request – so you acknowledge an alarm with a fingertip, without even opening the app:</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 -u admin:YOUR_PASSWORD <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Title: Restart server?&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Actions: view, Open status, https://status.YOUR_DOMAIN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s2">&#34;A service is stuck – check the status board.&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://ntfy.YOUR_DOMAIN/serverkueche-alerts</span></span></code></pre></div>
</div>
<p>You can also comfortably click the same message together via the web client – handy for trying out all the options:</p>
<p><figure class="my-6"><img src="/en/tutorials/ntfy-push-notifications/ntfy-senden_hu_8cc55fb4c5410497.webp" srcset="/en/tutorials/ntfy-push-notifications/ntfy-senden_hu_2949ce1491f24ea8.webp 480w, /en/tutorials/ntfy-push-notifications/ntfy-senden_hu_8cc55fb4c5410497.webp 768w, /en/tutorials/ntfy-push-notifications/ntfy-senden_hu_9b9d5370e1527f86.webp 1200w, /en/tutorials/ntfy-push-notifications/ntfy-senden_hu_604a19aa5acfc82.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/ntfy-push-notifications/ntfy-senden_hu_ac9291c0a0871ff.webp"
    alt="ntfy dialog to send a message with fields for topic, title, priority and tags" title="The send dialog in the web client shows all options: title, priority, tags, attachments"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The send dialog in the web client shows all options: title, priority, tags, attachments</figcaption></figure></p>
<h3 id="step-6-messages-arrive">Step 6: Messages arrive</h3>
<p>In the web client (provided you&rsquo;re logged in there with your <code>admin</code> account as in step 4) and in parallel in the app, the messages appear immediately – with title, timestamp and color-highlighted priority:</p>
<p><figure class="my-6"><img src="/en/tutorials/ntfy-push-notifications/ntfy-dashboard_hu_ce218c4441ec8262.webp" srcset="/en/tutorials/ntfy-push-notifications/ntfy-dashboard_hu_f5f8a52518dcdf34.webp 480w, /en/tutorials/ntfy-push-notifications/ntfy-dashboard_hu_ce218c4441ec8262.webp 768w, /en/tutorials/ntfy-push-notifications/ntfy-dashboard_hu_8851d80989e2b0be.webp 1200w, /en/tutorials/ntfy-push-notifications/ntfy-dashboard_hu_4f93c1ba3d812079.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/ntfy-push-notifications/ntfy-dashboard_hu_ea1f91df1fea55d8.webp"
    alt="ntfy web interface with three received notifications in the subscribed topic" title="Received messages in the topic – with title, priority and emoji tags"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Received messages in the topic – with title, priority and emoji tags</figcaption></figure></p>
<p>By the way, you can install the web client in Chrome or Edge as an <strong>app</strong> (PWA): then you also get push notifications on the desktop without having to keep a tab open – handy when the computer is running anyway and you want to see alarms directly on screen.</p>
<h3 id="step-7-access-tokens-for-scripts-instead-of-a-password">Step 7: Access tokens for scripts instead of a password</h3>
<p>Storing a password in every cron job and backup script is ugly. Better is an <strong>access token</strong> that you can revoke individually at any time:</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 <span class="nb">exec</span> ntfy ntfy token add admin</span></span></code></pre></div>
</div>
<p>ntfy returns a token that starts with <code>tk_</code>. You then use it instead of <code>-u</code> as a bearer token:</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 -H <span class="s2">&#34;Authorization: Bearer tk_YOUR_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Title: Nightly backup&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s2">&#34;Restic backup ran without errors&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://ntfy.YOUR_DOMAIN/serverkueche-alerts</span></span></code></pre></div>
</div>
<p>This way you can cleanly build ntfy into your <a href="/en/tutorials/restic-backups/">Restic backups</a> (a message at the end of the backup script) or have <a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma</a> use it as a notification channel – Uptime Kuma knows ntfy as a built-in notification type.</p>
<h3 id="step-8-practical-example--a-backup-script-that-reports-in">Step 8: Practical example – a backup script that reports in</h3>
<p>ntfy becomes most useful when a script automatically reports in. The following pattern wraps an arbitrary command and sends, depending on the outcome, a quiet success or a loud failure message. Save it as <code>/opt/scripts/backup-notify.sh</code> and adjust the domain, token and the actual backup command:</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">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -uo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">NTFY_URL</span><span class="o">=</span><span class="s2">&#34;https://ntfy.YOUR_DOMAIN/serverkueche-alerts&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">NTFY_TOKEN</span><span class="o">=</span><span class="s2">&#34;tk_YOUR_TOKEN&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Your actual backup command goes here – the exit code decides the message:</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> restic backup /opt --tag nightly<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  curl -s -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$NTFY_TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s2">&#34;Title: Backup ok&#34;</span> -H <span class="s2">&#34;Tags: white_check_mark&#34;</span> -H <span class="s2">&#34;Priority: 2&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -d <span class="s2">&#34;Nightly backup ran without errors.&#34;</span> <span class="s2">&#34;</span><span class="nv">$NTFY_URL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  curl -s -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$NTFY_TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s2">&#34;Title: BACKUP FAILED&#34;</span> -H <span class="s2">&#34;Tags: rotating_light&#34;</span> -H <span class="s2">&#34;Priority: 5&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -d <span class="s2">&#34;The backup aborted with an error – please check immediately.&#34;</span> <span class="s2">&#34;</span><span class="nv">$NTFY_URL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div>
</div>
<p>Make it executable and test it once by hand:</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">chmod +x /opt/scripts/backup-notify.sh
</span></span><span class="line"><span class="cl">/opt/scripts/backup-notify.sh</span></span></code></pre></div>
</div>
<p>If it runs cleanly, you hang it in a cron job – this way you get a short confirmation every night and are woken with full priority on an error:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">crontab -e</span></span></code></pre></div>
</div>
<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">30 3 * * * /opt/scripts/backup-notify.sh</span></span></code></pre></div>
</div>
<p>The principle transfers to anything: a certificate expires, a disk fills up (check <code>df</code> in the script), a deployment is done. Always the same one-liner – a <code>curl</code> to your topic. Exactly this makes ntfy so practical: you don&rsquo;t have to integrate anything, and anything that can do HTTP can notify you.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The app reports &ldquo;Cannot connect&rdquo; or the web client stays empty.</strong> The <code>NTFY_BASE_URL</code> doesn&rsquo;t
match the called address. it must be exactly your public HTTPS URL (<code>https://ntfy.YOUR_DOMAIN</code>,
without a trailing slash). After a change, run <code>docker compose up -d</code> again.</p>
<p><strong><code>curl</code> returns <code>HTTP 401</code> or <code>403</code>.</strong> With <code>deny-all</code>, missing or wrong credentials are the most
common error. check the user/password (<code>docker exec ntfy ntfy user list</code>) and, when sending, supply
<code>-u USER:PASSWORD</code> or the bearer token. A <code>403</code> means the user exists but has no rights to this
topic – then grant rights with <code>ntfy access</code>.</p>
<p><strong>Messages arrive in the browser but not as push on the phone when the tab is closed.</strong> browser push
needs granted notification rights and an active service worker; that&rsquo;s unreliable once the tab is
closed. for real &ldquo;on the go&rdquo; push, use the ntfy app – it keeps the connection in the background.</p>
<p><strong><code>502 Bad Gateway</code> from Traefik.</strong> the container isn&rsquo;t ready yet or listens on the wrong port.
check <code>docker compose logs ntfy</code> and that the label <code>loadbalancer.server.port=80</code> is set – ntfy
listens on port 80 in the container (<code>NTFY_LISTEN_HTTP=&quot;:80&quot;</code>).</p>
<p><strong><code>429 Too Many Requests</code> with many messages in quick succession.</strong> ntfy limits the rate per sender
by default to prevent abuse. On a private server with your own scripts you rarely hit this – but a
loop script without a pause does. bundle messages instead of firing them every second, or raise the
limits specifically via the <code>NTFY_VISITOR_*</code> environment variables (described in the ntfy docs under
&ldquo;Rate limiting&rdquo;). <code>NTFY_BEHIND_PROXY: &quot;true&quot;</code> is a prerequisite so the limit applies per real IP
instead of per Traefik container.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Updates</strong> you pull within the pinned version like this:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/ntfy <span class="o">&amp;&amp;</span> docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p>For a jump to a new major version, read the release notes first and raise the image tag.</p>
<p><strong>Backups:</strong> ntfy stores users, access rights and the message cache in its SQLite files in the volume (<code>cache.db</code>, <code>auth.db</code>). Back up the contents regularly by packing the volume into an archive …</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 -v ntfy_data:/data -v /opt/backups:/backup alpine:3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar czf /backup/ntfy-data.tar.gz -C /data .</span></span></code></pre></div>
</div>
<p>… and include this archive in your <a href="/en/tutorials/restic-backups/">encrypted off-site backup with Restic</a>. The message cache is dispensable (push messages are ephemeral), but you don&rsquo;t want to rebuild the user and rights database after a failure.</p>
<p><strong>In everyday use</strong> ntfy is nearly maintenance-free. Occasionally check with <code>docker exec ntfy ntfy user list</code> who has access, and revoke no-longer-used tokens with <code>ntfy token remove USER TOKEN-ID</code> (the IDs are shown by <code>ntfy token list USER</code>) – this way your push server stays your own.</p>
]]></content:encoded></item></channel></rss>