For years I used Keel to automatically update container images in my Kubernetes clusters. It worked, but as I moved to GitOps with ArgoCD, Keel’s push-based approach became a liability. I migrated to Renovate for PR-based image updates, and it’s been a significant improvement.
The Problem with Keel#
Keel watches for new container images and updates deployments directly in the cluster. You can configure it via annotations:
metadata:
annotations:
keel.sh/policy: major
keel.sh/trigger: pollWhen a new image appears, Keel modifies the deployment in-place.
Why This Broke Down#
GitOps drift - Keel updates the cluster, but not git. ArgoCD sees drift and wants to revert to what’s in git. You end up fighting your own tools.
No review process - Updates happen automatically. A bad image goes live immediately. Rolling back means finding the old tag and manually updating.
Notification-only visibility - Keel can send Slack notifications, but there’s no audit trail, no PR history, no way to see what changed when.
Limited version control - Keel’s policies (major/minor/patch) are annotation-based and hard to customize per-image.
Renovate: PR-Based Updates#
Renovate takes a different approach. It scans your repository, finds container images, checks for updates, and opens pull requests. The update only happens when you merge the PR.
This fits GitOps perfectly - git remains the source of truth.
My Renovate Setup#
Self-Hosted CronJob#
I run Renovate as a Kubernetes CronJob rather than using the hosted service:
apiVersion: batch/v1
kind: CronJob
metadata:
name: renovate
namespace: renovate
spec:
schedule: "0 */4 * * *" # Every 4 hours
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: renovate
image: ghcr.io/renovatebot/renovate:42.19.5
env:
- name: RENOVATE_CONFIG_FILE
value: /config/renovate.json
- name: RENOVATE_TOKEN
valueFrom:
secretKeyRef:
name: renovate-env
key: RENOVATE_TOKEN
volumeMounts:
- name: renovate-config
mountPath: /config
volumes:
- name: renovate-config
configMap:
name: renovate-configWhy self-hosted:
- Full control over scan frequency
- No rate limits from Renovate’s hosted service
- Works with private registries
- Runs inside my cluster, close to my git server
Configuration#
The repository-level renovate.json controls what gets updated:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"enabledManagers": ["helm-values", "kustomize", "kubernetes"],
"kubernetes": {
"managerFilePatterns": [
"/(^|/)base/.+/(?:[^/]*deployment)\\.ya?ml$/"
]
},
"prConcurrentLimit": 20,
"prHourlyLimit": 0,
"packageRules": [
// ... rules
]
}The managerFilePatterns is crucial - it tells Renovate to only look at deployment files in my base/ directory. This prevents it from trying to update the same image in multiple overlay locations.
Package Rules: The Power Feature#
This is where Renovate really shines. You can define complex rules for how different images should be handled.
Pinning Major Versions#
Some apps I want to stay on a specific major version until I’m ready to upgrade:
{
"description": "Keep linuxserver/lidarr on 2.x",
"matchDatasources": ["docker"],
"matchPackageNames": ["lscr.io/linuxserver/lidarr"],
"allowedVersions": "<3.0.0"
}Automerging Safe Updates#
Minor and patch updates for stable images can merge automatically:
{
"description": "Automerge minor/patch for Linuxserver images",
"matchDatasources": ["docker"],
"matchPackageNames": ["lscr.io/linuxserver/*"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "pr"
}The PR is still created (for visibility), but it merges without manual intervention.
Handling Weird Versioning#
LinuxServer images sometimes use date-based tags like 2021.12.15. These confuse semver parsing:
{
"description": "Ignore Linuxserver date-style 2021 tags",
"matchDatasources": ["docker"],
"matchPackageNames": [
"lscr.io/linuxserver/tautulli",
"lscr.io/linuxserver/overseerr"
],
"allowedVersions": "!/^2021\\./"
}Avoiding .0 Releases#
Home Assistant .0 releases are often buggy. I skip them:
{
"description": "Home Assistant: avoid .0 releases",
"matchDatasources": ["docker"],
"matchPackageNames": ["ghcr.io/home-assistant/home-assistant"],
"allowedVersions": "!/\\.0$/"
}I also disable major/minor updates entirely for Home Assistant - I want to control those manually:
{
"description": "Home Assistant: disable major/minor updates",
"matchPackageNames": ["ghcr.io/home-assistant/home-assistant"],
"matchUpdateTypes": ["major", "minor"],
"enabled": false
}The Workflow Now#
- Renovate scans every 4 hours
- PRs are created for available updates
- I review (or automerge handles it)
- Merge to master
- ArgoCD syncs the new image to the cluster
Everything flows through git. I can see exactly when an image was updated, who approved it, and what the previous version was.
Dependency Dashboard#
Renovate creates an issue called “Dependency Dashboard” that shows:
- Pending updates waiting for PRs
- Open PRs awaiting merge
- Updates blocked by version constraints
- Errors from failed lookups
This single issue gives you visibility into your entire update backlog.
Migration Tips#
1. Start with Automerge Disabled#
Get comfortable with the PR flow before enabling automerge:
{
"extends": ["config:recommended", ":automergeDisabled"]
}2. Use Semantic Commits#
Enable semantic commits for cleaner git history:
{
"semanticCommits": "enabled"
}PRs get titles like fix(deps): update lscr.io/linuxserver/sonarr to v4.0.2.
3. Limit Concurrent PRs Initially#
Don’t flood yourself with PRs on the first run:
{
"prConcurrentLimit": 5
}Increase once you’ve caught up on the backlog.
4. Remove Keel Gradually#
I removed Keel annotations from one app at a time, verified Renovate was creating PRs, then moved to the next. Don’t rip out Keel all at once.
Keel vs Renovate: Summary#
| Aspect | Keel | Renovate |
|---|---|---|
| Update method | Direct cluster modification | Pull requests |
| GitOps compatible | No (causes drift) | Yes |
| Review process | None (notification only) | Full PR review |
| Version constraints | Basic annotation policies | Powerful regex rules |
| Rollback | Manual | Git revert |
| Audit trail | Logs only | Full git history |
| Automerge | N/A (always auto) | Optional per-package |
Result#
My image updates now have:
- Visibility - PRs show exactly what’s changing
- Control - Rules prevent unwanted major updates
- History - Git log shows every update
- Safety - Automerge only for trusted packages
- Consistency - GitOps stays clean, no drift
The slight delay (PR review vs instant deploy) is worth the reliability. When something breaks, I know exactly which merge caused it and can revert cleanly.
If you’re running GitOps with ArgoCD or Flux, Renovate is the right choice for container image updates. Keel solved a problem for the pre-GitOps era, but PR-based updates are the modern approach.