Automated Kubernetes Deployments with Keel — No CI/CD SSH Required
Automated Kubernetes Deployments with Keel — No CI/CD SSH Required
I had a problem: my Next.js site runs in a container on my home Kubernetes cluster, and GitHub Actions builds a new image on every push to main. But how do you get that new image deployed to a cluster that isn't exposed to the internet?
The obvious answer — giving GitHub SSH access to my cluster — felt wrong. Opening inbound access to a home lab just for deployments is a security trade-off I didn't want to make.
The solution: Keel, a lightweight in-cluster controller that polls your container registry and automatically updates deployments when new images appear.
The Setup
My deployment pipeline already looked like this:
- Push to
main - GitHub Actions builds the Docker image
- Image pushed to GHCR as
ghcr.io/rfxtech/rfox.net:latest
The missing piece was step 4: getting the cluster to notice and pull the new image.
Installing Keel
Keel installs via Helm in about 30 seconds:
helm repo add keel https://charts.keel.sh
helm repo update keel
helm install keel keel/keel \
--namespace keel \
--create-namespace \
--set helmProvider.enabled=false \
--set polling.enabled=true \
--set polling.defaultSchedule='@every 1m'
Key settings:
helmProvider.enabled=false— I'm not using Helm-managed deployments for my appspolling.enabled=true— Keel actively checks the registry for changespolling.defaultSchedule='@every 1m'— Check every minute
Configuring Registry Auth
My GHCR packages are private, so Keel needs credentials to poll for new digests. I created a secret in the keel namespace with my GHCR token:
kubectl -n keel create secret generic keel-registry \
--from-literal=username=rfxtech \
--from-literal=password=<ghcr-pat> \
--from-literal=registry=ghcr.io
Then told Keel to use it:
helm upgrade keel keel/keel \
--namespace keel \
--set helmProvider.enabled=false \
--set polling.enabled=true \
--set polling.defaultSchedule='@every 1m' \
--set secret.name=keel-registry \
--set secret.create=false
Annotating the Deployment
The magic is three annotations on your deployment:
kubectl annotate deployment rfox-net \
keel.sh/policy=force \
keel.sh/trigger=poll \
keel.sh/match-tag=true
keel.sh/policy=force— Update on any change (since we're using:latest, this triggers on digest changes)keel.sh/trigger=poll— Use polling to detect changeskeel.sh/match-tag=true— Only update images that match the current tag
Switching to the latest Tag
One important detail: my deployment was previously pinned to a specific SHA tag (sha-b2267c2). For Keel's polling to work with digest comparison, I switched to the latest tag:
kubectl set image deployment/rfox-net nginx=ghcr.io/rfxtech/rfox.net:latest
Combined with imagePullPolicy: Always on the container, Kubernetes will always pull fresh when Keel triggers an update.
The Result
The full flow is now:
git push origin main- GitHub Actions builds and pushes
ghcr.io/rfxtech/rfox.net:latest - Within 1 minute, Keel detects the new image digest
- Keel triggers a rolling update on the deployment
- New pods pull the fresh image and start serving traffic
Zero inbound access to the cluster. Keel reaches out to GHCR — the registry never needs to reach in. No webhooks, no SSH keys in CI, no VPN tunnels.
Why Not Flux/ArgoCD?
For a single app on a home lab, Keel hits the sweet spot:
- Single Helm install — no CRDs, no multi-component architecture
- Annotation-based — no extra manifests to maintain
- Works with existing CI — no changes to my GitHub Actions workflow were needed
- Minimal resources — one pod, barely any memory
Flux and ArgoCD are fantastic for multi-team GitOps at scale. For "I just want my home site to auto-deploy," Keel is the right tool.
Gotchas
- Private registries need explicit auth — Keel doesn't inherit
imagePullSecretsfrom your deployments. You need to provide credentials separately. - Use
imagePullPolicy: Always— Without this, Kubernetes might use a cached image even after Keel updates the deployment. :latesttag is fine here — Yes,:latestin production is generally discouraged. But with Keel + digest comparison, it's actually the cleanest pattern. Keel tracks the actual digest, not the tag name.
Final Thoughts
The best deployment pipeline is one you don't have to think about. Push code, go make coffee, come back to a live site. Keel makes that possible without compromising your cluster's security posture.