Keel: Automated Kubernetes Deployments Without GitOps Overhead
Image-driven rollouts for the homelab cluster that doesn't need Argo

Contents
The orthodox answer to “how should my Kubernetes cluster deploy updates” is GitOps: Argo CD or Flux watching a Git repository, reconciling the cluster to match declared state, the whole pull-based pipeline. It is genuinely the right answer for a team shipping a product. For a homelab cluster running Sonarr, a couple of dashboards, and the odd hobby project, it is also a small mountain of YAML, a controller to babysit, and a Git workflow you have to actually follow. Sometimes you just want third-party images to update themselves when a new tag lands, without standing up a GitOps engine to do it.
That’s exactly the gap Keel fills. It’s a single lightweight controller that watches your workloads, notices when a newer image is available, and updates the deployment for you. No CRDs to learn for basic use, no Git repo, no reconciliation loop to reason about — you annotate a deployment and Keel handles the rest. I run it for the “keep my media stack current” jobs that would be absurd to wire into a full GitOps pipeline, and it earns its keep quietly.
How Keel decides to update
Keel works off the image references already in your manifests plus a handful of annotations that tell it which updates to accept. The key concept is a policy: do you want every new tag, only patch releases, only minor releases, or a specific tag whose underlying digest changes (the latest case)? Getting the policy right is the whole game — too loose and a major version bump breaks something at 3am; too strict and you’re back to updating by hand.
The policies map cleanly onto semantic versioning:
major— take any newer version, including breaking onesminor—1.4.0→1.5.0but never2.0.0patch—1.4.0→1.4.1only, the safe defaultforce— re-pull when a fixed tag likelatestorstablepoints at a new digestall— accept anything, including pre-releases
You attach a policy with annotations on the workload:
| |
That’s the whole contract. keel.sh/policy: minor says “track minor and patch bumps.” keel.sh/trigger: poll with a schedule tells Keel to check the registry hourly, because not every registry can push notifications. When a new minor tag appears, Keel updates the deployment’s image and lets Kubernetes do its normal rolling update. Crucially, it relies on your image tag being semver — a tag like latest or nightly has no ordering, so for those you use the force policy and digest comparison instead of version comparison.
Two ways to find out about new images
Keel supports two discovery mechanisms and you pick per workload. Polling is the universal fallback: Keel periodically queries the registry for tags matching the policy. It works against any registry, including Docker Hub and the linuxserver.io GHCR mirror, at the cost of a little latency and some registry traffic. Set the schedule sensibly — hourly is plenty for a homelab, and polling every minute is a good way to hit Docker Hub’s rate limits and get temporarily throttled.
Webhooks are the push alternative. If your registry or CI can call Keel’s HTTP endpoint when an image is built, updates happen the instant the image lands rather than at the next poll. Keel exposes a native webhook plus first-class handlers for Docker Hub, Quay, and GCR. For a homelab that mostly consumes upstream images you didn’t build, polling is usually what you want; webhooks earn their keep when you are the one building and pushing.
Installing it
Keel ships a Helm chart and the install is refreshingly small. The whole thing is one controller deployment with RBAC to watch and patch workloads:
| |
By default Keel also exposes a small web UI and an admin endpoint, plus a Prometheus /metrics endpoint so you can graph how many updates it’s processed. Set Slack or generic webhook notifications and you get a message every time it rolls something forward, which turns “did anything change overnight” into a glance rather than an audit. If you already run a lightweight monitoring stack — I’m partial to Beszel for exactly this kind of no-Grafana-overhead setup — Keel’s metrics endpoint slots straight in.
Beyond Deployments: Helm, StatefulSets, and the rest
The annotation approach above covers Deployments, DaemonSets, StatefulSets, and CronJobs — Keel patches the image on any of them the same way. What often trips people up is Helm-managed workloads. If a release was installed with Helm and you patch the live Deployment directly, your next helm upgrade will happily revert Keel’s change, because Helm’s stored values still point at the old tag. Keel knows this and offers a Helm provider that updates the release values instead of the raw object, so the change survives the next upgrade.
For a homelab I usually disable the Helm provider (--set helmProvider.enabled=false, as above) and manage a small set of plain Deployments, because it’s simpler to reason about. But if your whole cluster is Helm releases, turn it on and annotate via the chart’s values rather than the rendered manifest:
| |
The mental model is the same either way — a policy plus a trigger — but the object Keel edits differs, and matching that to how the workload was deployed is what prevents the “it updated, then un-updated itself” confusion.
Approvals, so it doesn’t surprise you
Letting a controller mutate production whenever upstream tags a release is, reasonably, terrifying for some workloads. Keel has a built-in approval workflow for exactly this. Annotate a deployment with a required vote count and Keel will prepare the update but hold it until you approve, via the UI or a Slack button:
| |
Now your media stack can self-update on patch with no ceremony, while your reverse proxy or database sits behind a one-click approval so nothing changes the front door without a human nodding first. You can check pending approvals from the CLI against Keel’s API, or just click the Slack message. This is the setting that lets you turn automation up on the disposable stuff and keep it firmly manual on the load-bearing stuff, in the same cluster.
Troubleshooting: why it “isn’t updating”
The single most common Keel complaint is “it’s not picking up my new image,” and it’s almost always one of three things.
Non-semver tags with a semver policy. A minor or patch policy needs comparable version tags. If your image is tagged latest or 2024-11-01, Keel can’t order it — switch that workload to keel.sh/policy: force and it’ll re-pull when the digest under the fixed tag changes. Check the Keel logs (kubectl logs -n keel deploy/keel); it says plainly when it skips a tag it can’t parse.
Polling too rarely, or a rate-limited registry. If nothing’s happening, confirm the poll schedule is set and that Keel isn’t being throttled by Docker Hub. The logs show the registry queries and any 429 responses.
RBAC or namespace scope. By default Keel watches cluster-wide, but if you’ve scoped it to specific namespaces it simply won’t see workloads elsewhere. And if the controller can’t patch deployments, it’ll detect the update and fail to apply it — the log line is explicit about the RBAC denial.
Drift you can’t explain. Because Keel writes directly to live objects, kubectl get deploy may show an image your manifests don’t. That’s not a bug; it’s the entire design, and it’s the crux of the next section.
It updated something you didn’t want it to. If Keel is rolling forward a workload you meant to pin, check for a stray cluster-wide default policy or an annotation you copied between manifests without thinking. The fix is to be explicit: set keel.sh/policy: never (or simply remove the annotations) on anything that should stay put. Keel only acts on workloads it’s told to, so an over-eager update almost always traces back to an annotation you didn’t mean to leave on.
A note on trusting upstream tags
Keel automating your rollouts means Keel is, in effect, trusting whoever controls the upstream image tags. That’s a fine trade for the linuxserver.io stack and other well-run community images, but it’s worth being clear-eyed about: a force policy on a mutable latest tag means whatever gets pushed to that tag runs in your cluster, unreviewed, at the next poll. For images you didn’t build, that’s a supply-chain surface. The patch/minor policies are safer precisely because they only move within a version line, and the approval workflow is your circuit-breaker for anything load-bearing.
If that makes you twitch, the answer isn’t to abandon automation — it’s to pin the risky workloads to immutable, verifiable images and gate them behind an approval, while letting the disposable stuff self-update freely. Keel gives you both dials on the same controller, which is exactly why it suits a mixed homelab: you don’t have to pick one posture for the whole cluster.
Where Keel ends and GitOps begins
Be honest about the boundary. Keel mutates live cluster objects directly — it does not write changes back to Git, so your manifests and your running state can drift. There’s no audit trail beyond Keel’s logs and notifications, no pull-request review, no easy git revert of a bad rollout. If multiple people manage the cluster, or you need an authoritative record of what’s deployed and why, that drift is exactly the problem GitOps exists to solve, and you should reach for Flux or Argo instead. I’ve written up the pull-based, Git-as-source-of-truth approach in GitOps with Flux; read that if the drift above makes you nervous, because for some clusters it absolutely should.
If your objection to GitOps is the templating pain rather than the model itself, Kustomize plus Flux is a lighter path than the full Helm-and-Argo stack, and keeps Git authoritative.
Verdict
Keel is the right tool when you have a small, mostly single-operator cluster full of third-party images and you want them to keep themselves current without you wiring up a GitOps pipeline. Set patch on the noisy stuff, gate the load-bearing services behind an approval, point a Slack webhook at it, and you’ve automated 90% of your update toil in an afternoon with one controller. The moment you need an audit trail, multi-person review, or Git as the source of truth, you’ve outgrown it — and that’s fine, because by then you’ll know you have. For the homelab it’s near-ideal; for a team shipping a product, it’s the wrong shape and you should skip straight to Flux or Argo.




