bumpsight
v0.5.8
Published
Docker image update advisor for self-hosters. Lints docker-compose files for anti-patterns, checks registries for newer tags in the same family, and uses a local LLM (Ollama) to summarize breaking changes from upstream release notes.
Downloads
879
Maintainers
Readme
bumpsight
Docker image update advisor and applier for self-hosters. Periodically scans your compose.yaml files, classifies new tags as patch / minor / major, applies the safe ones automatically, and emails you the rest with one-click approve / deny links — accompanied by an LLM-summarised read of the upstream release notes.
Watchtower was archived 2025-12-17. Diun and What's Up Docker tell you a tag moved but can't tell you whether the bump is safe or apply it for you. bumpsight does both — and stays out of your way for the bumps you don't want it touching.
What you get
- Daemon mode — one container, one config block, runs forever. Polls every
interval. Auto-discovers everycompose.yamlunder/stacks. - Semver-aware policy on two axes. Each stack has an
appaxis (the primary service) and adependenciesaxis (Postgres / Redis / MariaDB / Vault / etc.). Each axis takespatch/minor/major/notify/none. Default since v0.5.1:{ app: minor, dependencies: none }— auto-apply patches + minors on the app (the bumps semver flags as backwards-compatible), hold majors for approval, silent on deps (they follow the parent app's release cadence, not their own). Set globally and override per stack. - One-click approve / deny. Emails contain real URLs that, when clicked, pull and recreate the affected service via the host's Docker socket — or mark it denied and never bother you about that bump again.
- LLM-assisted risk read for held bumps via any OpenAI-compatible LLM endpoint — LiteLLM (cloud fan-out), Ollama (local), OpenAI, vLLM, anything else that speaks
/v1/chat/completions. - SMTP and Apprise notifiers built in. Apprise inherits its 70+ channels (Discord, ntfy, Slack, Gotify, …) without bumpsight having to embed them.
- CLI commands for the audit-style work:
doctor(lint),scan(one-shot tag check),advise(LLM summary). Run from your terminal, no daemon needed.
Quick start (Docker)
The drop-in:
services:
bumpsight:
image: ghcr.io/miller-joe/bumpsight:latest
container_name: bumpsight
restart: unless-stopped
ports:
- "9100:9100"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Aligned mount: container path == host path. Required so target
# stacks with relative bind mounts (./config, ./data, …) resolve
# correctly when bumpsight invokes `docker compose` against them.
- /mnt/docker/stacks:/mnt/docker/stacks # mount your compose tree
- bumpsight-state:/var/lib/bumpsight # SQLite state lives here
- ./bumpsight.yaml:/config/bumpsight.yaml:ro # optional, see below
environment:
BUMPSIGHT_NOTIFY: "smtp://user:[email protected]:587/[email protected]&[email protected]"
BUMPSIGHT_PUBLIC_URL: "https://bump.example.com"
# Default since v0.5.1: auto-apply patch+minor on the app axis, hold
# majors for approval, silent on deps. Override per-stack in
# bumpsight.yaml. To restore the pre-v0.5.0 "ask about everything"
# behavior, set BUMPSIGHT_AUTO_UPDATE_APP: "notify".
BUMPSIGHT_INTERVAL: "6h"
# Any OpenAI-compatible LLM endpoint. See "LLM endpoint" below.
BUMPSIGHT_LLM_URL: "http://litellm:4000/v1"
BUMPSIGHT_LLM_KEY: "sk-..."
BUMPSIGHT_MODEL: "smart"
volumes:
bumpsight-state:That's the whole product. Mount your compose tree at the same path inside the container as on the host (the aligned-mount convention since v0.4.2 — keeps relative bind mounts in target stacks resolvable when bumpsight runs docker compose against them) and bumpsight auto-discovers every <stack>/compose.{yaml,yml} underneath. Set stacks_dir: in bumpsight.yaml to the same path you mounted, or override via BUMPSIGHT_STACKS_DIR. Point BUMPSIGHT_PUBLIC_URL at however you expose port 9100 (reverse proxy, Tailscale, LAN-only) — that's the base URL bumpsight uses for the approve/deny links it embeds in your emails.
To opt a specific stack OUT of scanning, set its policy to none in bumpsight.yaml (see below). To restrict to a specific allowlist instead of auto-discovery, pass paths after daemon or set compose_files: in the config.
Quick start (CLI only)
If you don't want a daemon — just point-in-time audits — use the npm package:
npx bumpsight doctor compose.yaml
npx bumpsight scan compose.yaml
npx bumpsight advise linuxserver/sonarr:4.0.14 --to 4.1.0Requires Node 20+. The advise command needs an LLM endpoint configured (see below); everything else works offline.
LLM endpoint
bumpsight talks to any OpenAI-compatible chat-completions endpoint. Three common setups:
LiteLLM (recommended for self-hosters without a GPU)
LiteLLM proxies a single OpenAI-compatible interface in front of Cerebras / Groq / Mistral / Gemini / OpenRouter / Anthropic / OpenAI / etc. Most have free tiers generous enough for bumpsight's needs (a single 6h scan on ~50 stacks is a few thousand tokens at most). Once LiteLLM is up:
environment:
BUMPSIGHT_LLM_URL: "http://litellm:4000/v1"
BUMPSIGHT_LLM_KEY: "sk-..." # LiteLLM master key
BUMPSIGHT_MODEL: "smart" # or whichever LiteLLM alias you've set upOllama (local, requires GPU)
Ollama speaks the OpenAI compat API natively at /v1 since 0.1.40. No key needed.
environment:
BUMPSIGHT_LLM_URL: "http://ollama:11434/v1"
BUMPSIGHT_MODEL: "qwen2.5:14b-instruct"(Legacy OLLAMA_HOST is also accepted — bumpsight derives <host>/v1 automatically.)
OpenAI / direct provider
environment:
BUMPSIGHT_LLM_URL: "https://api.openai.com/v1"
BUMPSIGHT_LLM_KEY: "sk-..."
BUMPSIGHT_MODEL: "gpt-4o-mini"Same shape works for any other provider that exposes /v1/chat/completions — vLLM, llama.cpp's server, OpenRouter direct, Together AI, Groq direct, etc. BUMPSIGHT_LLM_URL unset = advise disabled (held emails arrive without the LLM section). Everything else still works.
Configuration
Three sources, in precedence order: CLI flags > environment variables > /config/bumpsight.yaml.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
| BUMPSIGHT_NOTIFY | (none) | Comma-separated list of notifier URIs. See "Notification channels" below. |
| BUMPSIGHT_PUBLIC_URL | (none) | Public-facing base URL of the daemon. Approve/deny links are only included in notifications when this is set. |
| BUMPSIGHT_AUTO_APPLY | (unset) | Legacy single-axis default — applies to the app axis only since v0.5.0 (pre-v0.5.0 it set both axes). Use BUMPSIGHT_AUTO_UPDATE_APP / BUMPSIGHT_AUTO_UPDATE_DEPENDENCIES for fine-grained control. |
| BUMPSIGHT_AUTO_UPDATE_APP | minor | Default app-axis policy: patch / minor / major / notify / none. v0.5.1 default auto-applies patches + minors and holds majors for approval. |
| BUMPSIGHT_AUTO_UPDATE_DEPENDENCIES | none | Default deps-axis policy. v0.5.0+ silences dep images by default; deps follow the parent app's cadence. Set to notify if you want bumpsight to surface dep tag changes. |
| BUMPSIGHT_INTERVAL | 6h | Scan interval. 30s, 10m, 6h, 1d. |
| BUMPSIGHT_STACKS_DIR | /stacks | Root directory for auto-discovery (one level deep). |
| BUMPSIGHT_CONFIG | /config/bumpsight.yaml | Path to the YAML config file. |
| BUMPSIGHT_DB | /var/lib/bumpsight/state.db | SQLite state file. |
| BUMPSIGHT_HTTP_PORT | 9100 | Approve/deny server port. |
| BUMPSIGHT_HTTP_HOST | 0.0.0.0 | Bind interface. |
| BUMPSIGHT_LLM_URL | (none) | OpenAI-compatible LLM base URL ending in /v1. When unset, advise is skipped. |
| BUMPSIGHT_LLM_KEY | (none) | Bearer token for the LLM endpoint. Required for LiteLLM, OpenAI, etc.; ignored by Ollama. |
| BUMPSIGHT_MODEL | llama3.2 | Model name. For Ollama: e.g. qwen2.5:14b-instruct. For LiteLLM: an alias like smart. |
| BUMPSIGHT_LLM_TIMEOUT_MS | 180000 | Per-call LLM request timeout (ms). Default 180s since v0.4.2. Routers like LiteLLM walk fallback chains server-side and can exceed shorter timeouts; bump higher for slow local Ollama on CPU, lower for stricter SLAs. |
| OLLAMA_HOST | (none) | Legacy Ollama base URL. Used as <host>/v1 when BUMPSIGHT_LLM_URL is unset. |
| GITHUB_TOKEN | (none) | Optional. Lifts the GitHub-anonymous rate limit when fetching upstream release notes. |
| BUMPSIGHT_DIGEST_HOUR | 18 | Hour-of-day (0–23, local TZ) the daily-digest email fires. Set to a negative value (-1) to disable. Empty days produce no email. |
| BUMPSIGHT_OUTBOX_DIR | /var/lib/bumpsight/outbox | Where every dispatched notification is archived as JSON (per-event + daily-digest). |
| BUMPSIGHT_OUTBOX_KEEP | 200 | Most recent N outbox files retained; older ones unlinked on every write. |
| BUMPSIGHT_PRUNE_SCHEDULE | (unset) | Opt-in deep prune interval — 24h, 7d, etc. When set, bumpsight runs docker image prune --filter until=168h -af, docker volume prune -f, and docker builder prune -af on that interval and logs total reclaimed bytes. Off by default. |
| BUMPSIGHT_WATCH_INTERVAL | (scan interval) | Poll cadence for watched_releases — 6h, 1d, etc. Only used when watched_releases is configured. Defaults to BUMPSIGHT_INTERVAL. |
/config/bumpsight.yaml
Optional. Useful for per-stack overrides and committing your apply policy to git.
# v0.5.0+ two-axis form. v0.5.1 default: { app: minor, dependencies: none } —
# auto-apply patch+minor on the primary service, hold majors for approval,
# silent on deps.
default:
app: minor # auto patch+minor on the app, hold majors for approval
dependencies: none # silent — deps follow the parent app's release cadence
stacks:
stalwart: { app: none, dependencies: none } # never auto-bump
authentik: { app: none, dependencies: none } # only ever apply manually
glance: { app: major } # let dashboards float everything
postgres: { app: patch } # patches yes, minors hold
# Legacy single-axis form is still accepted (mapped to `{ app: <value>,
# dependencies: none }` since v0.5.0). To restore the pre-v0.5.0 "ask about
# every bump" behavior on both axes, use:
#
# default: { app: notify, dependencies: notify }
interval: 6h
notify:
- smtp://user:[email protected]:587/[email protected]&[email protected]
- apprise://apprise.local:8000/notify/bumpsight # extra channels via apprise-api
# stacks_dir: /stacks # override the auto-discovery root if needed
# compose_files: [] # explicit allowlist; when set, bypasses auto-discovery
public_url: https://bump.example.com
# Optional (v0.5.7): watch non-Docker upstreams that ship as GitHub Releases —
# a binary you pin by hand (git-lfs, a CLI), a tool in a build:-only container,
# anything with no compose image: line for the scanner to see. Notify-only.
# watch_interval: 1d # poll cadence (defaults to `interval`)
# watched_releases:
# - repo: git-lfs/git-lfs # owner/repo on GitHub (required)
# current: "3.7.0" # the version you have installed (required)
# name: git-lfs # optional display label (defaults to repo name)
# policy: notify # notify (default) | none (disable this entry)
# include_prerelease: false # optional; track pre-releases tooThe stack name is the basename of the directory holding the compose file — /stacks/jellyfin/compose.yaml → stack jellyfin.
By default bumpsight scans every <stacks_dir>/<name>/compose.{yaml,yml} it finds. To opt a stack out, set its policy to none. Hidden directories (starting with .) are skipped automatically — that gives you a quick archive convention.
Watched releases (non-Docker upstreams)
The scanner only sees Docker images referenced by an image: key in a discovered compose file. Versions that live anywhere else are invisible to it — a binary baked into a Dockerfile by a hardcoded version pin (GIT_LFS_VERSION=3.7.0), a tool installed into a build:-only container, a CLI you drop into /usr/local/bin. There's no registry tag for the scanner to compare, so those silently fall behind.
watched_releases (v0.5.7, opt-in) covers them. Declare the upstream GitHub repo and the version you currently have installed; bumpsight polls GitHub Releases on watch_interval and emails when a newer release appears. It's notify-only — bumpsight can't install a host binary, so there are no Approve/Deny links. The email tells you what's new (with the usual LLM release-note summary when an LLM is configured) and reminds you to update the pin yourself, then bump current: for that entry. Each newer release fires exactly one email until you update current or a newer one lands. Set policy: none to silence an entry without removing it; pre-releases are ignored unless include_prerelease: true.
Notification channels
Two drivers ship in the box; you can mix and stack them.
SMTP / SMTPS
smtp://user:[email protected]:587/[email protected]&[email protected]
smtps://user:[email protected]/[email protected]&[email protected]&[email protected]Multiple ?to= recipients are allowed. HTML email with an action card at the top (Approve / Deny buttons), plain-text fallback included. Implicit TLS on smtps:// (port 465 by default), STARTTLS via opportunistic upgrade on smtp:// (port 587 by default).
Apprise
apprise://apprise.example.com/notify/bumpsight
apprises://apprise.example.com/notify/bumpsight # forces httpsThese point at an existing apprise-api instance — the URL is the endpoint apprise-api exposes. Once you've configured the underlying targets in apprise-api (Discord, ntfy, Slack, Gotify, Mattermost, …), bumpsight POSTs Markdown-formatted notifications to that endpoint and apprise fans them out. bumpsight does not embed apprise itself, so the bumpsight image stays slim.
Stacking
BUMPSIGHT_NOTIFY: "smtp://...,apprise://apprise.local/notify/bumpsight"Comma-separated. Failures in one channel never block delivery to the others.
How apply works
When a scan finds a new tag in the same family, bumpsight:
- Classifies the bump as
patch/minor/major/unknownagainst the previous tag. - Decides based on the policy for that stack (or the default).
- Auto-apply path: rewrites the compose file to swap only the tag (preserving comments, formatting, other services), then runs
docker compose -f <file> pull <service>followed by... up -d <service>against the host's Docker socket. The combined log is stored in the SQLite state.- Failure is non-destructive (v0.5.6+): if the
pull/up -dstep fails, bumpsight rolls the compose file back to its pre-apply tag and marks the rowfailed. A failed apply never leaves the compose pinned to a tag that wasn't successfully pulled — that drift would be invisible until the nextup/reboot, and a bad target tag would otherwise poison every future recreate. The stack stays on its last-known-good image; re-triggering the bump re-applies cleanly. (Before v0.5.6 only paired-dep bundled applies rolled back; a plain single-service bump left the rewrite in place.)
- Failure is non-destructive (v0.5.6+): if the
- Hold path: sends an HTML email with the action card at top — instruction + styled Approve / Deny buttons — followed by metadata and the LLM release-note summary.
https://your-bump-url/approve/<token>— when clicked, marks the row approved and runs the same apply path as above.https://your-bump-url/deny/<token>— marks the row denied. bumpsight will not re-prompt for this exact bump.
- Post-apply prune (v0.4.2+): after a successful, non-moving-tag apply, bumpsight removes the just-replaced image tag if no other container references it. Reports
freed N MBin the apply log + completion email. Always best-effort; a prune failure never marks the apply itself failed. Skipped for moving-tag bumps (:latestdigest changes etc. — the rolling tag still resolves the old digest implicitly). This keeps disk usage from creeping up over time as bumpsight applies multiple version bumps in succession. - Scheduled deep prune (v0.5.2+, opt-in): set
BUMPSIGHT_PRUNE_SCHEDULE=7d(or any interval) and bumpsight runsdocker image prune --filter until=168h -af,docker volume prune -f, anddocker builder prune -afon that cadence. Cleans up dangling layers from cancelled builds, orphaned anonymous volumes, and the buildx cache — all things the targeted post-apply prune deliberately leaves alone. Logs total reclaimed bytes per pass. Off by default; per-step failures don't abort the next step or stop the schedule.
Rolling-tag (:latest, :nightly, …) semantics
When the source compose entry uses a moving tag (:latest, :stable, :edge, :nightly, :rolling, etc.), bumpsight tracks updates by digest rather than tag string. The compose file is left untouched on apply — docker compose pull picks up the new digest and up -d recreates the container. v0.4.2 fixed a class of apply failures where digest-only bumps on rolling tags were trying (and failing) to rewrite a 12-char digest prefix into a compose entry that read latest.
unknown bumps (cross-family changes, channel rolls like latest → stable) are always held, regardless of policy. There's nothing meaningful to "auto-patch" there.
The (stack, service, current_tag, target_tag) tuple is unique in state — repeat scans don't re-spam notifications for already-seen bumps.
CLI commands
The daemon owns the long-running flow. The CLI commands let you do the same checks ad-hoc.
bumpsight doctor <compose-file>
Lints a compose.yaml for homelab anti-patterns. Exit code 1 on errors, 0 otherwise.
$ bumpsight doctor compose.yaml
compose.yaml:
ERROR BS002 [jellyfin] service runs with privileged: true
WARN BS001 [radarr] image linuxserver/radarr uses implicit or explicit :latest tag
WARN BS008 [portainer] mounts the Docker socket
INFO BS004 [radarr] no healthcheck defined
summary: 1 error, 2 warn, 1 info--json for machine-readable output.
bumpsight scan <compose-file>
For each image, checks Docker Hub or ghcr.io for the highest tag in the same family.
$ bumpsight scan compose.yaml
compose.yaml: 4 service(s) with images
jellyfin linuxserver/jellyfin:10.10.7 → 10.11.0
radarr linuxserver/radarr:5.14.0.9383-ls250 up to date
postgres postgres:16 up to date--offline skips the lookup. --timeout <ms> sets the per-image budget. --json for machine output.
bumpsight advise <image> --to <tag>
Resolves the upstream GitHub repo for the image, fetches releases between the two tags (capped at the 25 most recent in range to keep prompts manageable), feeds them to your configured LLM endpoint, and prints a structured summary of breaking changes, new features, and required actions. Pass --compose <file> --service <name> and the LLM also gets your service config so it can call out env-vars or ports specific to your setup.
bumpsight ships a curated upstream-repo table for the common Docker Official images (node → nodejs/node, postgres → postgres/postgres, vault → hashicorp/vault, etc.) so the advise output isn't blank for them. For everything else it falls back to scanning the Docker Hub description for a GitHub link, or you can pass --repo owner/name explicitly.
bumpsight daemon
The same loop the container runs, but you can run it bare-metal too — useful for cron-driven setups (bumpsight daemon --once) or systemd services.
Lint rules
| ID | Severity | Rule |
|---|---|---|
| BS001 | warn | Image uses implicit or explicit :latest tag |
| BS002 | error | Service runs with privileged: true |
| BS003 | warn | Service uses network_mode: host |
| BS004 | info | No healthcheck defined |
| BS005 | warn | Environment variable looks like a secret with a literal value |
| BS006 | info | No restart policy set |
| BS007 | info | No memory limit configured (mem_limit or deploy.resources.limits.memory) |
| BS008 | warn | Mounts the Docker socket |
| BS010 | warn | cap_add contains a dangerous capability |
Rule IDs are stable across releases. Suppression via ignore-file is on the roadmap.
Development
git clone https://github.com/miller-joe/bumpsight
cd bumpsight
npm install
npm run dev -- daemon /path/to/compose.yaml --once
npm testRequires Node 20+.
To build the container image locally:
docker build -t bumpsight:dev .
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/stacks bumpsight:dev daemon /stacks/some/compose.yaml --onceRoadmap
Shipped:
- v0.1:
doctor(lint),scan(registry tag freshness),advise(LLM-summarised breaking changes) - v0.2:
daemonmode — interval scheduler, semver-aware auto-apply policy, SQLite state, SMTP / Apprise notifiers, HTTP approve/deny server, automatic compose-file rewrite +docker compose pull && up -d, GHCR image (linux/amd64 + linux/arm64), HTML emails with action card at top, OpenAI-compatible LLM client (LiteLLM / Ollama / OpenAI / etc.), curated upstream-repo table for Docker Official images - v0.3:
:latest-digest tracking with semver-pair resolution (Phase 1+2),/queueHTTP route,reportpolicy, LSIO tag format support, dependency-image-aware advise prompts, GHCR per-tag manifest support, LLM opinion-fallback when no upstream notes, multi-arch buildx via GHCR cache - v0.4: split policy (
appvsdependenciesaxes), apply-completion notifications + outbox archive +advise_textpersistence (v0.4.1), advise reliability (180s default timeout, configurableBUMPSIGHT_LLM_TIMEOUT_MS, retry-on-AbortError), aligned-mount convention, rolling-tag apply path fix, post-apply targeted image prune (v0.4.2), daily-digest email rollup at configurable hour with<details>/<summary>per-row collapsibles (v0.4.3) - v0.5: BREAKING DEFAULT — new policy fallback
{ app: minor, dependencies: none }since v0.5.1 (auto patch+minor on the app, hold majors, silent deps; pre-v0.5.0 was{ notify, notify }). Paired dep-recommendation lookup — when advising on a held app-major bump, fetches the parent app's upstream compose at the new tag and surfaces dep-pin diffs in the advise email (bump/image-change/addrecommendations). v0.5.2: opt-in scheduled deep-prune viaBUMPSIGHT_PRUNE_SCHEDULE(image + volume + builder prune on a configurable interval).
Planned:
- Digest-bump enrichment via OCI labels — resolve
org.opencontainers.image.revisionto upstream git SHA, diff commits between previous + new SHAs, feed to LLM for a real "what changed in this digest move" summary - Apply-time bundling of paired dep changes — let Approve on a major bundle the dep pin rewrites alongside the app rewrite, atomically
- Rule ignore-file for
doctor - Podman and
nerdctlsocket support quay.ioregistry- Multi-hop family walks (e.g.
4.0.14→ through4.0.x→4.1.xbreakage map)
License
MIT
Support
If this saves you a broken homelab update at 3 AM:
