diff --git a/README.md b/README.md index 9d13c78..0e799a6 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Each links to its own README for variables, ports, and storage. | [alloy](alloy/README.md) | Grafana Alloy shipping host and Docker telemetry to Grafana Cloud | | [code-server](code-server/README.md) | VS Code in the browser, as a remote dev box | | [couchbase](couchbase/README.md) | Couchbase Server | +| [diun](diun/README.md) | Image-update notifier, reading the Docker API through a read-only proxy | | [gitea-mirror](gitea-mirror/README.md) | Gitea + PostgreSQL + gitea-mirror, mirroring GitHub repos | | [opencode-web](opencode-web/README.md) | opencode coding agent, served as a browser UI | | [openhands](openhands/README.md) | OpenHands coding agent, running each session in a container it spawns | diff --git a/diun/.env.example b/diun/.env.example new file mode 100644 index 0000000..f42c21f --- /dev/null +++ b/diun/.env.example @@ -0,0 +1,9 @@ +DIUN_NOTIF_TELEGRAM_TOKEN= +DIUN_NOTIF_TELEGRAM_CHATIDS= +DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true +DIUN_WATCH_SCHEDULE=0 */6 * * * +DIUN_WATCH_WORKERS=10 +DIUN_WATCH_JITTER=30s +TZ=UTC +LOG_LEVEL=info +LOG_JSON=false diff --git a/diun/README.md b/diun/README.md new file mode 100644 index 0000000..c69844b --- /dev/null +++ b/diun/README.md @@ -0,0 +1,62 @@ +# diun + +[Diun](https://crazymax.dev/diun/) watches the images of every running +container and sends a Telegram message when one has an update. It only +notifies — it never pulls or restarts anything. + +Two containers: `diun` itself, and `dockerproxy`, which hands it a read-only +slice of the Docker API. + +## Docker API access + +Diun needs the Docker API to enumerate containers and read the image reference +each one runs. Mounting `/var/run/docker.sock` into it directly would be +host-root-equivalent — the API has no read/write split, so anything that can +talk to the socket can create a privileged container that mounts `/`. Adding +`:ro` to the mount does not help: it stops the socket *file* being replaced, not +the API being used. + +So the socket is mounted into +[tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) +instead, and Diun reaches it over the compose network at +`tcp://dockerproxy:2375`. `POST` is revoked by default in that image, so +container create, `exec`, start and kill return 403. A compromised Diun image +can no longer become root on the host — which matters because Diun is the one +service here whose whole job is to talk to the daemon. + +Exactly two API sections are granted, both verified against a live watch cycle: + +| Variable | Why | +| --- | --- | +| `CONTAINERS` | enumerate running containers | +| `IMAGES` | `ImageInspect` on each container's image — without it every image logs `403 Forbidden` and nothing is analysed | + +`INFO`, `NETWORKS`, `VOLUMES` and the rest stay revoked; a watch cycle runs +clean without them. + +## Environment + +`DIUN_NOTIF_TELEGRAM_TOKEN` and `DIUN_NOTIF_TELEGRAM_CHATIDS` are required and +fail fast if unset. Everything else has a working default. + +| Variable | Default | Purpose | +| --- | --- | --- | +| `DIUN_NOTIF_TELEGRAM_TOKEN` | — | Bot token from @BotFather | +| `DIUN_NOTIF_TELEGRAM_CHATIDS` | — | Comma-separated chat ids to notify | +| `DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT` | `true` | Watch every container without per-container labels | +| `DIUN_WATCH_SCHEDULE` | `0 */6 * * *` | Cron for the watch cycle | +| `DIUN_WATCH_WORKERS` | `10` | Parallel registry lookups | +| `DIUN_WATCH_JITTER` | `30s` | Random delay before each job | +| `TZ` | `UTC` | Timezone for the schedule and log timestamps | +| `LOG_LEVEL` / `LOG_JSON` | `info` / `false` | Logging | + +State lives in the `diun-data` volume (`DIUN_DB_PATH=/data/diun.db`). Diun +notifies on first sight of an image, so a fresh volume produces one round of +notifications for everything currently running. + +## Version pinning + +`crazymax/diun:4.33` rather than `:latest`. Diun holds Docker API access, so an +unreviewed image change is the highest-leverage supply-chain step on the host; +the proxy bounds what a bad image could do, and the pin means an image only +changes when this file does. diff --git a/diun/compose.yml b/diun/compose.yml new file mode 100644 index 0000000..f4a4584 --- /dev/null +++ b/diun/compose.yml @@ -0,0 +1,38 @@ +# Required env vars (set in Coolify/Dokploy, or a sibling .env): +# DIUN_NOTIF_TELEGRAM_TOKEN, DIUN_NOTIF_TELEGRAM_CHATIDS + +services: + diun: + image: crazymax/diun:4.33 + command: serve + environment: + DIUN_PROVIDERS_DOCKER: "true" + DIUN_PROVIDERS_DOCKER_ENDPOINT: tcp://dockerproxy:2375 + DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "${DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT:-true}" + DIUN_NOTIF_TELEGRAM_TOKEN: ${DIUN_NOTIF_TELEGRAM_TOKEN:?required} + DIUN_NOTIF_TELEGRAM_CHATIDS: ${DIUN_NOTIF_TELEGRAM_CHATIDS:?required} + DIUN_DB_PATH: /data/diun.db + DIUN_WATCH_SCHEDULE: "${DIUN_WATCH_SCHEDULE:-0 */6 * * *}" + DIUN_WATCH_WORKERS: "${DIUN_WATCH_WORKERS:-10}" + DIUN_WATCH_JITTER: "${DIUN_WATCH_JITTER:-30s}" + TZ: "${TZ:-UTC}" + LOG_LEVEL: "${LOG_LEVEL:-info}" + LOG_JSON: "${LOG_JSON:-false}" + volumes: + - diun-data:/data + depends_on: + - dockerproxy + + # Read-only slice of the Docker API. POST is revoked by default in this image. + dockerproxy: + image: tecnativa/docker-socket-proxy:v0.5.0 + environment: + CONTAINERS: 1 + IMAGES: 1 + security_opt: + - no-new-privileges:true + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + diun-data: