<?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>Diun – Serverküche</title><link>https://serverkueche.de/en/tags/diun/</link><description>Diun – 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>Mon, 27 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/diun/index.xml" rel="self" type="application/rss+xml"/><item><title>Keeping your whole Docker stack safely up to date</title><link>https://serverkueche.de/en/tutorials/keep-docker-stack-updated/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/keep-docker-stack-updated/</guid><description>Update discipline for your Docker stack: pin versions, get notified about updates with Diun, apply them safely, clean up – instead of blind auto-updates.</description><content:encoded><![CDATA[<p>A self-hosted stack is never &ldquo;done&rdquo;. Container images get security updates, apps get new
features – and whoever doesn&rsquo;t keep up eventually runs vulnerable software. This recipe
turns &ldquo;I should update sometime&rdquo; into a reliable routine: informed, controlled and with
a safety net.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>Not a tool, but <strong>update discipline</strong> for your entire Docker stack. By the end you have:
<strong>pinned versions</strong> (never <code>latest</code> again), an <strong>update notifier</strong> (Diun) that tells you
<em>when</em> something new is available, a <strong>safe rollout routine</strong> with a backup beforehand
and a rollback plan – and a cleanup rhythm so old images don&rsquo;t clutter your disk. Tested
with <strong>Diun v4.33.0</strong> on Docker 29 / Compose v5.3.</p>
<p>The guiding idea is deliberately <strong>not</strong> &ldquo;update everything automatically&rdquo;. With stateful
apps (databases, Nextcloud, Immich) an unattended update in the middle of the night can
trigger a migration that goes wrong – and nobody is there. The safe way is:
<strong>pin → get notified → apply deliberately → verify</strong>. We treat blind auto-updates
honestly at the end (step 6): they have their place, but a small one.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Basic knowledge of <a href="/en/tutorials/docker-compose-basics/">Docker Compose</a> – you should
be able to read tags, volumes and <code>compose.yaml</code>.</li>
<li>A running stack you want to maintain (e.g. the apps behind your
<a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy</a>).</li>
<li>A working <a href="/en/tutorials/restic-backups/">backup strategy with Restic</a> – it&rsquo;s the
safety net that makes updates relaxed in the first place.</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/keep-docker-stack-updated/" data-content-piece="VPS 2000 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 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">A well-maintained multi-app stack including databases feels noticeably happier on the VPS 2000.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-2000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 2000 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-never-latest-again--pin-versions-deliberately">Step 1: Never <code>latest</code> again – pin versions deliberately</h3>
<p>The most common mistake appears in countless guides: <code>image: nextcloud:latest</code>. The
problem: <code>latest</code> is a moving target. A <code>docker compose pull</code> can pull you a new major
version with breaking changes at any time, unasked – you never know what you&rsquo;re running,
and a rollback is barely possible.</p>
<p>Pin every service to a <strong>fixed tag</strong> instead. Two sensible levels:</p>
<ul>
<li><strong>Major tag</strong> (<code>postgres:18</code>, <code>nextcloud:34-apache</code>): automatically gets patch and
minor updates of the same major version on the next <code>pull</code>, but <strong>never</strong> a major
jump. A good compromise for most services.</li>
<li><strong>Exact version</strong> (<code>vaultwarden/server:1.37.1</code>): full control, nothing changes without
your involvement. Ideal for delicate, stateful apps.</li>
</ul>
<p>What you&rsquo;re running <strong>right now</strong> is shown by Compose per 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"><span class="nb">cd</span> ~/YOUR_APP <span class="o">&amp;&amp;</span> docker compose images</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">CONTAINER           REPOSITORY          TAG                 PLATFORM            IMAGE ID            SIZE                CREATED
</span></span><span class="line"><span class="cl">diun-demo-whoami    traefik/whoami      v1.11.0             linux/amd64         200689790a0a        3.04MB              17 months ago</span></span></code></pre></div>
</div>
<p>If <code>latest</code> appears anywhere in the <code>TAG</code> column, that&rsquo;s your first candidate to pin.
Enter the specific tag into the <code>compose.yaml</code> – that&rsquo;s the foundation for everything
else.</p>
<p>Which version is currently <strong>stable</strong> you look up at the source, not by gut feeling: the
<strong>tags overview on Docker Hub</strong> (or GHCR) of the image, or the <strong>release notes</strong> on
GitHub. For official images this also works via API:</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 <span class="s2">&#34;https://hub.docker.com/v2/repositories/library/postgres/tags/?page_size=20&#34;</span> <span class="p">|</span> grep -oE <span class="s1">&#39;&#34;name&#34;:&#34;[0-9.]+&#34;&#39;</span></span></span></code></pre></div>
</div>
<p>Take the highest stable version of the same series you want to run – pre-releases
(<code>rc</code>, <code>beta</code>) stay out.</p>
<h3 id="step-2-see-whats-outdated--the-update-notifier-diun">Step 2: See what&rsquo;s outdated – the update notifier Diun</h3>
<p>Pinning means updates no longer come on their own. So you need someone to <strong>let you
know</strong> when a new image is available. That&rsquo;s exactly what <strong>Diun</strong> (Docker Image Update
Notifier) does – it updates <strong>nothing</strong>, it only watches and notifies. Give it its own
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 ~/diun <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/diun</span></span></code></pre></div>
</div>
<p>The <code>compose.yaml</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">diun</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">crazymax/diun:4.33.0</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="s2">&#34;TZ=Europe/Berlin&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;DIUN_WATCH_SCHEDULE=0 8 * * *&#34;</span><span class="w">          </span><span class="c"># daily at 8 a.m.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;DIUN_PROVIDERS_DOCKER=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=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">./data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock:ro</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></span></code></pre></div>
</div>
<ul>
<li><strong><code>DIUN_PROVIDERS_DOCKER=true</code></strong> lets Diun discover your running containers via the
Docker socket.</li>
<li><strong><code>WATCHBYDEFAULT=false</code></strong> means Diun only watches containers you <strong>explicitly</strong> mark –
so you don&rsquo;t get a flood about things that don&rsquo;t interest you.</li>
<li><strong><code>DIUN_WATCH_SCHEDULE</code></strong> is a cron expression; once a day is plenty and spares the
registry rate limits.</li>
</ul>
<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>The Docker socket is not a harmless read access
  </p>
  <div class="prose-kitchen text-sm">We mount the socket here with <code>:ro</code>, and that sounds reassuring – but it&rsquo;s only half
true. The <code>:ro</code> merely prevents the <em>file</em> <code>docker.sock</code> from being overwritten; through
the Docker API behind it you can still do anything: start containers with <code>--privileged</code>,
mount the host filesystem, in short <strong>root on the host</strong>. Whoever has access to the
socket is practically root-equivalent – regardless of the <code>:ro</code>. For a pure notifier
like Diun this is a deliberately accepted risk; if you want to defuse it, don&rsquo;t attach
Diun directly to the socket, but to an upstream
<strong><a href="https://github.com/Tecnativa/docker-socket-proxy">docker-socket-proxy</a></strong> that only
passes through the few endpoints (list containers, read images) Diun really needs.</div>
</div>
<p>Mark the containers Diun should keep an eye on via a label in <strong>their</strong> <code>compose.yaml</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="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;diun.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;diun.watch_repo=true&#34;</span></span></span></code></pre></div>
</div>
<p><code>diun.enable=true</code> switches on monitoring. <code>diun.watch_repo=true</code> lets Diun search the
<strong>whole repository</strong> for newer tags (e.g. whether a <code>19</code> already exists for your
<code>postgres:18</code>) – without this label, Diun only checks whether your <em>exact</em> tag got a new
image. Start Diun and watch the first run:</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 class="o">&amp;&amp;</span> docker compose logs -f diun</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">INF Found 1 image(s) to analyze provider=docker
</span></span><span class="line"><span class="cl">INF New image found      image=docker.io/traefik/whoami:v1.11.0 provider=docker
</span></span><span class="line"><span class="cl">INF New image found      image=docker.io/traefik/whoami:v1.12.0 provider=docker
</span></span><span class="line"><span class="cl">INF New image found      image=docker.io/traefik/whoami:latest-arm64 provider=docker
</span></span><span class="line"><span class="cl">INF Jobs completed       added=97 failed=0 skipped=0 unchanged=0 updated=0</span></span></code></pre></div>
</div>
<p>The <strong>first</strong> run is just the inventory: Diun writes every tag it finds into its database as
&ldquo;New image found&rdquo; (<code>added</code>). That <strong>one</strong> watched container yields 97 entries is down to
<code>watch_repo</code> – Diun then fetches the manifest for <em>every</em> tag in the repository, including
architecture variants like <code>latest-arm64</code> or <code>v1.10.0-armv7</code>.</p>
<p>Two things follow from this that are easy to get wrong:</p>
<ul>
<li>
<p><strong>The first run does not notify.</strong> The option <code>watch.firstCheckNotif</code> defaults to <code>false</code>, and
that applies per image tag. Everything Diun sees for the first time lands in the database
silently. It only reports once something <strong>changes</strong> later on.</p>
</li>
<li>
<p><strong><code>watch_repo</code> costs registry requests.</strong> 97 manifest queries per run almost blow the Docker
Hub limit without a login (100 requests per IPv4 address or IPv6 <code>/64</code> subnet) on their own.
On the second run of the same day our test ran straight into it – <code>StatusCode: 429</code> and
<code>failed=36</code> in the log. How much budget is left – and which window the registry is currently
counting in – it tells you itself:</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="nv">TOKEN</span><span class="o">=</span><span class="k">$(</span>curl -s <span class="s2">&#34;https://auth.docker.io/token?service=registry.docker.io&amp;scope=repository:ratelimitpreview/test:pull&#34;</span> <span class="p">|</span> cut -d<span class="s1">&#39;&#34;&#39;</span> -f4<span class="k">)</span>
</span></span><span class="line"><span class="cl">curl -sI -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">&#34;</span> https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest <span class="p">|</span> grep -i <span class="s2">&#34;^ratelimit&#34;</span></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">ratelimit-limit: 100;w=3600
</span></span><span class="line"><span class="cl">ratelimit-remaining: 98;w=3600</span></span></code></pre></div>
</div>
<p><code>w</code> is the window width in seconds. Docker <strong>documents</strong> a six-hour window; our test VPS
consistently got <code>w=3600</code>, i.e. one hour, in August 2026. So rely on the header rather than
on a remembered number – and note that this query itself costs a request. Beyond that,
narrow the tags down to what you actually care about – real release tags:</p>
</li>
</ul>
<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="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;diun.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;diun.watch_repo=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;diun.include_tags=^v1\\.1[12]\\.\\d+$$&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;diun.sort_tags=semver&#34;</span></span></span></code></pre></div>
</div>
<p><code>include_tags</code> is a regular expression. Two pitfalls hide in the notation: the backslashes have
to be <strong>doubled</strong> in YAML, and the trailing <code>$</code> is written as <code>$$</code> – otherwise Compose tries to
substitute an environment variable. With this label, exactly the two real release tags remain
out of the 97 entries:</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">INF Found 1 image(s) to analyze provider=docker
</span></span><span class="line"><span class="cl">INF New image found      image=docker.io/traefik/whoami:v1.11.0 provider=docker
</span></span><span class="line"><span class="cl">INF New image found      image=docker.io/traefik/whoami:v1.12.0 provider=docker
</span></span><span class="line"><span class="cl">INF Jobs completed       added=2 failed=0 skipped=0 unchanged=0 updated=0</span></span></code></pre></div>
</div>
<p>So that the notice doesn&rsquo;t just sit in the log, you attach a <strong>notification</strong>. Diun can
do email, Telegram, ntfy, Gotify and many more. For Telegram – you may already have set
up the bot in the <a href="/en/tutorials/uptime-kuma-monitoring/">Uptime Kuma tutorial</a> – two
lines in the <code>environment</code> block are enough:</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;DIUN_NOTIF_TELEGRAM_TOKEN=YOUR_BOT_TOKEN&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;DIUN_NOTIF_TELEGRAM_CHATIDS=YOUR_CHAT_ID&#34;</span></span></span></code></pre></div>
</div>
<p>All other channels are in the <a href="https://crazymax.dev/diun/config/notif/">Diun documentation</a>.
This is what the notice looks like – here via
<a href="/en/tutorials/ntfy-push-notifications/" data-track-event="Navigation|Tutorial-Link|/en/tutorials/ntfy-push-notifications/ ← /en/tutorials/keep-docker-stack-updated/">ntfy</a> in the browser, as soon as a
newer version appears for one of your pinned images:</p>
<p><figure class="my-6"><img src="/en/tutorials/keep-docker-stack-updated/diun-ntfy-benachrichtigung_hu_d67ae1a519b2a0b3.webp" srcset="/en/tutorials/keep-docker-stack-updated/diun-ntfy-benachrichtigung_hu_8129696206d8b55a.webp 480w, /en/tutorials/keep-docker-stack-updated/diun-ntfy-benachrichtigung_hu_d67ae1a519b2a0b3.webp 768w, /en/tutorials/keep-docker-stack-updated/diun-ntfy-benachrichtigung_hu_44140d0b16dd867d.webp 1200w, /en/tutorials/keep-docker-stack-updated/diun-ntfy-benachrichtigung_hu_38eee30b194c873.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/keep-docker-stack-updated/diun-ntfy-benachrichtigung_hu_4c9d8af228cf5fcf.webp"
    alt="The ntfy web interface shows a Diun notification titled &ldquo;docker.io/traefik/whoami:v1.12.0 is available&rdquo; with the note that the tag is available on docker.io via the docker provider" title="Diun&#39;s update notice: v1.12.0 has appeared for the pinned whoami v1.11.0"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Diun&#39;s update notice: v1.12.0 has appeared for the pinned whoami v1.11.0</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>Test the notification without waiting for weeks
  </p>
  <div class="prose-kitchen text-sm">Because the first run deliberately stays silent, you don&rsquo;t know after setting things up whether
the notification path works at all – in the worst case you find out only after missing an
important notice. So set <code>DIUN_WATCH_FIRSTCHECKNOTIF=true</code> in Diun&rsquo;s <code>environment</code> <strong>once</strong>,
delete <code>./data</code> and restart: then Diun sends notifications for the inventory too – exactly one
per tag found. Channel verified, remove the variable again afterwards (otherwise the next
<code>watch_repo</code> run floods your inbox).</div>
</div>
<h3 id="step-3-apply-an-update-safely-the-routine">Step 3: Apply an update safely (the routine)</h3>
<p>Diun reports an update – now comes the actual discipline. <strong>Never</strong> just pull blindly. The
fixed order for every service:</p>
<p><strong>1. Read the release notes.</strong> On a major jump (e.g. Nextcloud 34 → 35) this is mandatory:
are there breaking changes, migration steps, removed options? Two minutes here save hours
of debugging.</p>
<p><strong>2. Back up first.</strong> An update is the classic moment for something to break. Make a
<a href="/en/tutorials/restic-backups/">Restic backup</a> of the data (for big jumps, additionally a
<a href="/en/tutorials/netcup-snapshots-scp/">netcup snapshot</a>). Then a failure is only a
setback, not data loss.</p>
<p><strong>3. Bump the tag and pull.</strong> Set the new tag in the <code>compose.yaml</code> and fetch the image:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose pull</span></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"> Image traefik/whoami:v1.12.0 Pulled</span></span></code></pre></div>
</div>
<p><strong>4. Restart.</strong> Compose only replaces the affected container, the volumes stay:</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>
<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"> Container diun-demo-whoami Recreated
</span></span><span class="line"><span class="cl"> Container diun-demo-whoami Started</span></span></code></pre></div>
</div>
<p><strong>5. Verify.</strong> Does the container run cleanly (<code>docker compose ps</code>, <code>docker compose logs</code>),
and does the app still do what it should? Stateful apps may now run a database migration –
take a look at the log until it&rsquo;s through.</p>
<p><strong>6. Have a rollback in mind.</strong> If something goes wrong, set the tag in the <code>compose.yaml</code>
back to the old version and do <code>docker compose up -d</code> again. Because your data lives in the
<strong>volume</strong> (not in the container), for most apps this is a clean step back. Only if the new
version has <strong>already migrated the database</strong> does a tag rollback no longer help – then you
need the backup from step 2. That&rsquo;s exactly what it&rsquo;s for.</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>One app at a time
  </p>
  <div class="prose-kitchen text-sm">Don&rsquo;t update the whole stack at once. Go service by service and check each time that
everything runs before moving to the next. If something breaks then, you immediately know
which update was to blame.</div>
</div>
<h3 id="step-4-dont-forget-the-base--host-engine-hidden-images">Step 4: Don&rsquo;t forget the base – host, engine, hidden images</h3>
<p>Your apps are only half the battle. Equally important:</p>
<ul>
<li>
<p><strong>Operating system &amp; Docker engine.</strong> The OS and security packages of Debian you best
install <a href="/en/tutorials/unattended-upgrades-automatic-updates/">automatically with unattended-upgrades</a>.
The <strong>Docker engine is deliberately left out</strong> of that: unattended-upgrades by default
only pulls the Debian sources (<code>origin=Debian</code>), <strong>not</strong> the Docker APT repo
(<code>origin=Docker</code>, <code>download.docker.com</code>). So the engine isn&rsquo;t updated in the background
and won&rsquo;t unexpectedly restart all your containers at night – because an engine update
briefly <strong>takes running containers down</strong>. You&rsquo;d better determine that moment yourself and
update the engine specifically by hand when it fits:</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">sudo apt update <span class="o">&amp;&amp;</span> sudo apt upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span></span></code></pre></div>
</div>
<p>Afterwards check with <code>docker compose ps</code> that all stacks are up again.</p>
<p>If a brief container restart at night doesn&rsquo;t bother you, you can also let the engine come
along <strong>with</strong> unattended-upgrades: allow the Docker repo in
<code>/etc/apt/apt.conf.d/50unattended-upgrades</code> as an additional origins pattern. What matters
is <code>archive=</code> – the Docker repo sets no <code>Codename</code> field, the often-recommended
<code>codename=${distro_codename}</code> would run into the void and Docker would stay out despite
the entry:</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">&#34;origin=Docker,archive=${distro_codename}&#34;;</span></span></code></pre></div>
</div>
</li>
<li>
<p><strong>Hidden images.</strong> Many stacks contain databases, caches and helper services
(<code>postgres</code>, <code>redis</code>, <code>mariadb</code>, <code>gotenberg</code> …) that nobody thinks of. Diun watches them
automatically as soon as the respective container carries the <code>diun.enable</code> label – so
give it to <strong>all</strong> long-lived services, not just the visible main app.</p>
</li>
<li>
<p><strong>Extensions of your reverse proxy.</strong> Pinned plugin versions too (e.g. the
<a href="/en/tutorials/crowdsec-setup/">CrowdSec bouncer</a>) or Traefik itself want to be updated
deliberately.</p>
</li>
</ul>
<h3 id="step-5-clean-up--reclaim-storage">Step 5: Clean up – reclaim storage</h3>
<p>Every update leaves the <strong>old</strong> image behind. After a few months this adds up. What you&rsquo;re
using is shown by:</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 system df</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">TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
</span></span><span class="line"><span class="cl">Images          26        4         14.75GB   14.16GB (95%)
</span></span><span class="line"><span class="cl">Containers      4         4         45.06kB   0B (0%)
</span></span><span class="line"><span class="cl">Local Volumes   0         0         0B        0B
</span></span><span class="line"><span class="cl">Build Cache     36        0         828.2MB   828.2MB</span></span></code></pre></div>
</div>
<p>Unused, <strong>untagged</strong> (&ldquo;dangling&rdquo;) images are removed by the safe default 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">docker image prune</span></span></code></pre></div>
</div>
<p>Careful: this only removes dangling images. An <strong>old, still tagged</strong> version (like
<code>whoami:v1.11.0</code> after the update to <code>v1.12.0</code>) doesn&rsquo;t count as dangling and stays behind.
You reclaim that specifically:</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 rmi traefik/whoami:v1.11.0</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">Untagged: traefik/whoami:v1.11.0
</span></span><span class="line"><span class="cl">Deleted: sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab</span></span></code></pre></div>
</div>
<p>Whoever wants to clean up more radically uses <code>docker image prune -a</code> (removes <strong>all</strong>
images no running container currently uses). That&rsquo;s powerful, but pulls everything anew on
the next start – use it deliberately, not in cron.</p>
<p>The safe <code>docker image prune -f</code> (dangling only), on the other hand, you can run regularly
without worry – e.g. weekly via a systemd timer or cron. That way no junk piles up between
your update sessions in the first place, and you reclaim the storage automatically.</p>
<h3 id="step-6-auto-update--when-its-okay-and-when-not">Step 6: Auto-update – when it&rsquo;s okay (and when not)</h3>
<p>The question remains: why not everything fully automatic? The best-known tool for that is
<strong>Watchtower</strong> – it pulls new images and restarts containers on its own. Two catches:</p>
<ul>
<li><strong>The upstream is orphaned.</strong> The official <code>containrrr/watchtower</code> hasn&rsquo;t had a release
since <strong>v1.7.1 (2023-11-11)</strong> – and the repository was <strong>archived at the end of 2025
(2025-12-17)</strong>, i.e. mothballed for good. Whoever uses it better reaches for a maintained
fork like <code>nickfedor/watchtower</code>.</li>
<li><strong>Auto-update is dangerous for stateful apps.</strong> Lifting a database or Nextcloud over a
major jump unattended at night is the opposite of &ldquo;safe&rdquo;.</li>
</ul>
<p>Auto-update makes sense at most for <strong>uncritical, stateless</strong> services – and even then
rather in <strong>monitor mode</strong> (<code>WATCHTOWER_MONITOR_ONLY=true</code>), which only reports instead of
updating. For everything else, the deliberate routine from step 3 beats any automation
tool. That&rsquo;s exactly why <strong>Diun (notify) + manual update</strong> is the recommendation here, not
Watchtower (act).</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>Safe automation for Git users: Renovate
  </p>
  <div class="prose-kitchen text-sm">If your <code>compose.yaml</code> files live in a Git repo, there&rsquo;s a third way that combines
automation and control: <strong>Renovate</strong> (or Dependabot) detects the pinned <code>image:</code> tags and
automatically opens a <strong>pull request</strong> that bumps the tag – complete with a link to the
release notes. So the update doesn&rsquo;t happen secretly on the server, but as a <strong>change you
review and merge</strong> before rolling it out with <code>docker compose up -d</code>. That way the safe
&ldquo;apply deliberately&rdquo; step is preserved, only the tedious checking is gone.</div>
</div>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong><code>docker compose pull</code> pulls no new image, even though a new version exists.</strong> Your tag
points to a fixed version (<code>:1.37.1</code>) or to a major tag (<code>:18</code>), under which there&rsquo;d only be
a new <em>major</em>. <code>pull</code> only fetches what the <strong>same tag</strong> now points to. For a version jump
you have to bump the tag in the <code>compose.yaml</code> yourself.</p>
<p><strong>The app no longer starts after the update or throws database errors.</strong> Usually a breaking
change or a failed migration. Check <code>docker compose logs</code>, compare with the release notes.
Set the tag back to the old version and <code>up -d</code>; if the new version already migrated the
data, restore the backup from step 3.</p>
<p><strong>Diun reports nothing, even though updates exist.</strong> Check that the containers carry the
label <code>diun.enable=true</code> and Diun can read the socket (<code>DIUN_PROVIDERS_DOCKER=true</code>, socket
mounted). For newer <strong>version tags</strong> the container additionally needs <code>diun.watch_repo=true</code>
– without it, Diun only sees changes to the exactly pinned tag. And: whatever Diun sees for the
<strong>first</strong> time is only written to the database, not reported (<code>watch.firstCheckNotif</code> is
<code>false</code>) – to test the notification path, set <code>DIUN_WATCH_FIRSTCHECKNOTIF=true</code> once.</p>
<p><strong>The disk fills up, even though you regularly run <code>docker image prune</code>.</strong> <code>docker image prune</code> only clears dangling images, not the old <strong>tagged</strong> versions. Remove them
specifically with <code>docker rmi &lt;image&gt;:&lt;tag&gt;</code> or – with care – <code>docker image prune -a</code>. The
build cache (<code>docker builder prune</code>) can grow too.</p>
<p><strong>On pulling you get <code>toomanyrequests</code> / a Docker Hub rate limit.</strong> <code>diun.watch_repo=true</code>
on many images queries many tags. Set the watch interval less often (e.g. once daily) and
narrow with <code>diun.include_tags</code> to relevant versions instead of scanning the whole repo.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>The rhythm.</strong> Diun reports → you read the release notes → backup → update → verify. In
practice that&rsquo;s a manageable appointment <strong>once a month</strong>; critical security holes you
patch immediately. Honestly: safe self-hosting doesn&rsquo;t work entirely without manual work –
but 15 minutes a month is the deal.</li>
<li><strong>Fast movers first.</strong> Some projects release frequently and with breaking changes –
<a href="/en/tutorials/self-host-immich-photos/">Immich</a>, mailcow,
<a href="/en/tutorials/crowdsec-setup/">CrowdSec</a>,
<a href="/en/tutorials/monitoring-grafana-prometheus/">Grafana</a> and
<a href="/en/tutorials/self-host-nextcloud/">Nextcloud</a>. Those you look at first and more often,
quiet candidates (databases, Redis) less so.</li>
<li><strong>Backups are the core of this recipe.</strong> An update without a backup is a gamble; with
<a href="/en/tutorials/restic-backups/">Restic</a> at your back, every update becomes relaxed. Before
big jumps, additionally a <a href="/en/tutorials/netcup-snapshots-scp/">snapshot</a> – it brings the
whole server back in one go.</li>
<li><strong>Version your <code>compose.yaml</code> files.</strong> Whoever keeps their Compose files in a Git repo can
roll back not only data but also the <strong>configuration</strong> to any earlier state – and sees in
the history exactly which tag was bumped when.</li>
<li><strong>Keep the tools up to date too.</strong> Diun itself and your reverse-proxy plugins belong in the
update round as well – otherwise the maintainer doesn&rsquo;t maintain itself.</li>
</ul>
]]></content:encoded></item></channel></rss>