Contents

Diun: Container Update Notifications Done Right

A tiny watcher that tells you an image changed and then does absolutely nothing else

Contents

There is a category of homelab tool that earns its place by refusing to do the obvious thing. Diun — Docker Image Update Notifier — watches the registries your containers came from, notices when a tag you are running points at a different image than it did yesterday, and sends you a message. Then it stops. It will not pull. It will not restart anything. It has no opinion about your maintenance window.

That restraint is the entire pitch, and it took me two rebuilds to appreciate it.

The case against the automatic version

Advertisement

The tempting tool in this space is the one that pulls and restarts for you. I ran that setup for about a year. It worked flawlessly right up until a media server’s upstream image shipped a database schema change in a point release, restarted at 03:00, migrated its library, and started throwing errors that took me most of a Saturday to unpick. The container was newer. The service was down. Nobody had asked me anything.

The problem is structural. A tool that pulls on your behalf must decide, without you, that now is a fine moment for this specific service to be replaced by a different version of itself. It has no way to know that the database is mid-backup, that you are away for a week, or that the release notes contain the phrase “you must run the migration manually before starting”. I have argued this at more length in the piece on container image housekeeping, and my position has hardened since: automatic pulls belong to systems with tests and rollback, and a homelab has neither.

Diun sits on the other side of that line. It tells you. You decide. The gap between “there is an update” and “the update is applied” is where you read the changelog, and that gap is worth defending.

What it actually watches

Diun compares manifests, which is a more useful thing than comparing tags. When it first sees jellyfin/jellyfin:10.10.3, it records the digest that tag currently resolves to. On each poll it re-resolves the tag. If the digest changed — because upstream rebuilt the image on a patched base, or force-pushed the tag, or you are on latest and a new release landed — you get a notification.

That means it catches something a version-comparison tool misses entirely: silent rebuilds. Renovate, which I use for the compose repo itself and wrote about in Renovate for homelab compose files, tells you when 2.14.7 becomes 2.15.0. Diun tells you when 2.14.7 stopped being the image it was last week. Those are different questions, and both are worth asking. The two tools overlap less than you would expect, and I run both.

Diun can also watch tags it is not currently running, via a regex. Point it at postgres with include_tags: ["^16\\.\\d+$"] and it will tell you when a new 16.x appears, whether or not you have a container on it.

Setup

Advertisement

The whole deployment is one container and one YAML file. Diun stores its state in a small embedded database, so give it a volume or it will re-notify you about every image on every restart.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:
  diun:
    image: crazymax/diun:4.28.0
    command: serve
    volumes:
      - ./data:/data
      - ./diun.yml:/diun.yml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      TZ: Europe/Copenhagen
      LOG_LEVEL: info
    restart: unless-stopped

Note the :ro on the socket. It is a lie that makes people feel better — read-only on the socket mount does nothing to stop a process that can talk to the Docker API from creating a privileged container. Mounting the socket into any container is handing that container root on the host, and Diun genuinely only needs to list containers and read their image names. The correct answer is a socket proxy that filters the API down to GET /containers/json, which I will come back to at the end.

Then diun.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
watch:
  workers: 10
  schedule: "0 6 * * *"
  firstCheckNotif: false
  jitter: 30s

providers:
  docker:
    watchByDefault: false

notif:
  ntfy:
    endpoint: https://ntfy.mylab.local
    topic: homelab-updates
    priority: 3
    tags:
      - whale
    templateTitle: "{{ .Entry.Image }}"
    templateBody: |
      New digest for {{ .Entry.Image }}
      {{ if .Entry.Manifest.Created }}Built: {{ .Entry.Manifest.Created }}{{ end }}
      Platform: {{ .Entry.Manifest.Platform }}

Three settings deserve explanation.

firstCheckNotif: false stops it notifying you about all forty of your images the first time it runs. Set this before the first start, or spend a minute clearing your phone.

schedule: "0 6 * * *" is once a day at six. Diun’s default is every six hours, which is three notifications you cannot act on for every one you can. Registry manifests do not change often enough to warrant polling faster than you are willing to respond.

watchByDefault: false is the important one, and it inverts the tool’s behaviour: Diun watches nothing unless a container asks to be watched. Which brings us to labels.

Labels are the good bit

With watchByDefault: false, you opt containers in individually:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:
  vaultwarden:
    image: vaultwarden/server:1.32.7
    labels:
      diun.enable: "true"
      diun.watch_repo: "true"
      diun.include_tags: '^1\.\d+\.\d+$'
    restart: unless-stopped

  some-abandoned-thing:
    image: ghcr.io/someone/thing:0.4.1
    restart: unless-stopped

The second service gets no notifications, forever, which is correct — it has not had a release since 2023 and I have made my peace with it.

diun.watch_repo: "true" widens the check from “has this exact tag changed” to “have any tags matching my filter appeared”, which is how you hear about 1.33.0 while running 1.32.7. The include_tags regex is doing real work there: without it, watch_repo will cheerfully tell you about testing, alpha, and every date-stamped nightly the maintainer pushes.

The label approach scales in the direction homelabs actually grow. Every new stack declares its own notification policy in the same file as everything else about it, and there is no central list to keep in sync. When I delete a stack, its Diun config goes with it.

Delivery goes to ntfy, which is where every other homelab alert of mine lands. Diun supports around a dozen notifiers — Gotify, Telegram, Matrix, mail, generic webhooks, Slack-shaped things — and they all take the same template variables. Sending updates to a dedicated topic rather than the general alerts topic matters more than it sounds: update notices are informational, and mixing them with “the NAS is on fire” trains you to swipe both away.

What a week actually looks like

Numbers make this concrete. Forty-one watched images, one check a day, over a recent seven-day stretch: eleven notifications. Of those, four were genuine upstream releases I went and read about. Five were base-image rebuilds where the application code was untouched — an Alpine security patch rippling through everything built on it, arriving as five separate messages within about twenty minutes. One was a maintainer force-pushing a tag to fix a broken build. One was a nightly that had slipped past my tag filter.

Eleven messages a week is close to the ceiling of what I will keep reading. Push it much higher and the tool stops working, because the real failure mode of a notifier is the habit of dismissal that volume creates. Everything in the configuration above is aimed at that number: the daily schedule, the tag regexes, the opt-in labels, the separate ntfy topic.

The five-messages-in-twenty-minutes cluster is worth dwelling on. Diun has no concept of “these are all the same event”. When a popular base image gets patched, everything you run that is built on it moves digest within the same CI window, and you get one message per image. Lowering workers from 10 to 2 spreads the checks out, which makes the cluster arrive over several minutes rather than several seconds, and does nothing to reduce the count. I have not found a good fix. In practice I recognise the shape — a burst of unrelated images all moving at once means a base layer changed, and there is usually nothing for me to do beyond a docker compose pull at the weekend.

The jitter setting exists for a different reason: it staggers your requests so that every Diun instance configured from the same blog post does not hit Docker Hub on the same second. Thirty seconds is plenty, and it is politeness rather than self-interest.

Comparing it to the alternatives

Advertisement

Four tools claim this territory and they are less interchangeable than the feature lists suggest.

Watchtower is the famous one. It watches, pulls and restarts, and it can be configured to only notify with WATCHTOWER_MONITOR_ONLY=true. In monitor-only mode it does roughly what Diun does, with one important difference: Watchtower needs the Docker socket and the ability to pull images, because its architecture assumes it will act. Diun in file-provider mode needs neither. The tool that cannot act is a smaller liability than the tool that has been asked not to.

Portainer and friends show you an update indicator in a web UI. That is a pull model — you have to go and look — and the going and looking is exactly the behaviour that stops happening in month three. A notification arrives whether or not you remembered.

Renovate, as above, answers a different question, and answers it better than anything else: what changed, and here are the release notes. Its blind spot is that it only knows what your repo says. If a stack was deployed by hand and never committed, Renovate has never heard of it.

A shell script. Genuinely viable. skopeo inspect docker://image:tag prints the digest; compare it to podman image inspect and pipe the difference to a notification. Thirty lines, no daemon, no socket, and you own it. I ran that for a year before Diun and the only reason I stopped is the tag-regex handling, which is fiddly to get right and which Diun does properly.

The choice between them is really a choice about what you want to be true at 3am. Diun’s answer is “nothing happened, and there is a message waiting for you”. Watchtower’s is “something happened”. I have been on both sides of that and I know which one lets me sleep.

Troubleshooting

No notifications, no errors. Almost always watchByDefault: false with a missing diun.enable label. Run docker compose exec diun diun image list — it prints exactly what Diun believes it is watching. If your container is absent from that list, it is a label problem, not a notification problem.

Rate limited on Docker Hub. Manifest lookups count against the anonymous pull limit, and forty images checked four times a day adds up. Add credentials under a registry block:

1
2
3
4
5
6
regopts:
  - name: docker.io
    selector: image
    username: myaccount
    password: mytoken
    timeout: 20s

A free account’s limit is generous enough that a once-daily check never approaches it.

Constant notifications for one image. Some publishers rebuild on every commit to their CI config, so the digest genuinely moves several times a week while the version never changes. Either accept it, or drop watch_repo for that image and rely on tag comparison alone.

Private registry with a self-signed certificate. Diun will refuse it, correctly. Add insecure: true to the regopts entry if you must, or better, put the CA in the container’s trust store with a volume mount. I run an internal CA, so the second option costs nothing.

Notifications arrive but say nothing useful. The default template is terse. The template variables give you the image, the digest, the platform and the manifest creation date. Diun cannot give you release notes — it never looked at the project, only the registry. If you want changelogs in the message, you want Renovate.

The socket problem, honestly

Diun’s Docker provider needs the socket to enumerate containers and read labels. That is a real privilege escalation vector for a tool whose entire job is sending you a text message. The proportionate fix is a read-only socket proxy in front of it, exposing only CONTAINERS=1 and nothing else, so the worst a compromised Diun can do is learn what you are running.

Alternatively, skip the Docker provider entirely. Diun has a file provider that takes a static list of images:

1
2
3
providers:
  file:
    filename: /watch.yml
1
2
3
4
# /watch.yml
- name: vaultwarden/server:1.32.7
  watch_repo: true
- name: caddy:2.8.4

No socket, no host access, no attack surface worth naming. You lose label-based discovery and gain a list to maintain by hand. For a small, stable set of critical services that is a fine trade, and for anyone taking a homelab threat model seriously it is the honest default.

Is it worth it?

Diun is about 30 MB of RAM and one line of daily notification. Against that: it is the only thing in my setup that knows the difference between the image I deployed and the image that tag points at today. That question has no other answer available to me.

It suits you if you already accept that updates are a decision you make rather than a thing that happens to you. If you would rather everything simply stayed current without your involvement, Diun will annoy you within a fortnight — you will get a message, do nothing, and eventually mute the topic, at which point you have a daemon burning electricity to be ignored.

I would not run it as my only update mechanism. Paired with Renovate against a compose repo it covers both halves of the question: Renovate for “a new version exists and here is what changed”, Diun for “the image under your running container quietly moved”. The pair costs a container and a bot account, and together they have caught two silent base image rebuilds that mattered and roughly forty that did not. That ratio is fine. The alternative is finding out from an outage.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.