You changed a ConfigMap. Your app is still running stale config. You kubectl rollout restart deployment/my-app for the fourteenth time this week and wonder if this is what being a Kubernetes operator actually feels like.
Yeah. It is. Unless you install Reloader.
Stakater Reloader is a small controller that watches ConfigMaps and Secrets and automatically triggers rolling restarts on any Deployment, StatefulSet, or DaemonSet that consumes them. It’s one of those tools that should honestly ship with Kubernetes by default, but here we are.
Let’s fix the ConfigMap reload problem properly.
The Actual Problem (It’s Worse Than You Think)
When you mount a ConfigMap as a volume, Kubernetes does eventually update the mounted files — after the kubelet syncs, which defaults to around 60 seconds. So mounted files do update. Eventually. But your app probably isn’t watching the filesystem for changes. It read the config at startup and cached it in memory. The file changed; your process doesn’t care.
When you use envFrom to pull a ConfigMap in as environment variables? Those never update. Environment variables are set at container start. Period. You can change the ConfigMap ten times; the pod sees the original values until you restart it.
So the workflow ends up being:
kubectl edit configmap my-app-config# change some valuekubectl rollout restart deployment/my-app# wait for rolloutkubectl rollout status deployment/my-appEvery. Single. Time.
In a homelab this is mildly annoying. In a real environment with multiple apps and ConfigMaps, it becomes a full-time job. Reloader automates exactly this loop.
How Reloader Works
Reloader runs as a controller in your cluster. It watches for changes to ConfigMaps and Secrets. When it detects a change, it finds every Deployment/StatefulSet/DaemonSet that references that ConfigMap (either via volume mounts or envFrom), and patches that workload’s pod template with a checksum annotation — specifically, it injects or updates an environment variable called STAKATER_<RESOURCE_NAME>_CONFIGMAP with the SHA256 of the ConfigMap’s data.
Patching the pod template is what triggers the rolling restart. Kubernetes sees the pod spec changed and does what it always does: rolls out new pods, terminates old ones. Your app starts fresh with the new config.
The beauty of it: no custom admission webhooks, no init containers, no sidecar agents. Just a controller that patches annotations. It’s boring in the best possible way.
Install via Helm
Add the Stakater chart repo and install Reloader into its own namespace:
helm repo add stakater https://stakater.github.io/stakater-chartshelm repo updatehelm install reloader stakater/reloader \ --namespace reloader \ --create-namespaceAs of mid-2026, the latest stable is v1.2.x. Check helm search repo stakater/reloader for the current version. There’s no elaborate values file needed for a homelab — the defaults work fine.
Verify it’s running:
kubectl get pods -n reloaderNAME READY STATUS RESTARTS AGEreloader-7d6f9b4c8f-xk9pt 1/1 Running 0 42sDone. The controller is live and watching your cluster.
Annotating Your Workloads
Reloader uses annotations on the Deployment/StatefulSet/DaemonSet to decide what to watch. There are three modes, from broadest to most surgical.
Mode 1: Auto — Watch Everything Referenced
metadata: annotations: reloader.stakater.com/auto: "true"This tells Reloader: watch every ConfigMap and Secret that this Deployment references — volumes, envFrom, all of it. Any change to any of them triggers a restart.
Good for most homelab workloads. If you only have one or two ConfigMaps per app, just use this.
Mode 2: Search/Match — Opt-In from Both Sides
# On the Deployment:metadata: annotations: reloader.stakater.com/search: "true"# On the ConfigMap(s) you want to trigger restarts:metadata: annotations: reloader.stakater.com/match: "true"This is more explicit. The Deployment says “I’m watching for matches,” and the ConfigMap says “I’m worth watching.” Only ConfigMaps/Secrets annotated with match: "true" will trigger restarts on Deployments annotated with search: "true".
Useful when you have a shared ConfigMap that multiple services reference but you only want certain services to restart on changes.
Mode 3: Specific List — Name Your ConfigMaps Directly
metadata: annotations: configmap.reloader.stakater.com/reload: "my-config-1,my-config-2" secret.reloader.stakater.com/reload: "my-secret"Maximum control. You name exactly which ConfigMaps and Secrets should trigger a restart for this Deployment. Comma-separated list, no spaces.
Use this in production-like environments where you want zero ambiguity about what triggers what.
A Real Workflow
Here’s a concrete example. You’ve got a simple app that reads its config from a ConfigMap via envFrom.
The ConfigMap
apiVersion: v1kind: ConfigMapmetadata: name: my-app-config namespace: defaultdata: LOG_LEVEL: "info" CACHE_TTL: "300" FEATURE_FLAG_X: "false"The Deployment
apiVersion: apps/v1kind: Deploymentmetadata: name: my-app namespace: default annotations: reloader.stakater.com/auto: "true"spec: replicas: 2 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: myrepo/my-app:latest envFrom: - configMapRef: name: my-app-configTrigger the Reload
Edit the ConfigMap to flip that feature flag:
kubectl edit configmap my-app-config# Change FEATURE_FLAG_X from "false" to "true"Reloader detects the change within a few seconds. It patches the Deployment’s pod template with a checksum env var like:
env: - name: STAKATER_MY_APP_CONFIG_CONFIGMAP value: "a3f2c8d1..." # SHA256 of the ConfigMap dataThis triggers a rolling restart. Watch it happen:
kubectl get events --field-selector reason=SuccessfulCreate --sort-by='.lastTimestamp'LAST SEEN TYPE REASON OBJECT MESSAGE3s Normal SuccessfulCreate replicaset/my-app-7d9f4b8c6f Created pod: my-app-7d9f4b8c6f-xp2kl8s Normal Killing pod/my-app-6c8b4d7f9-mv3qt Stopping container my-appNo manual kubectl rollout restart. Reloader handled it.
The Helm Checksum Pattern (and Why Reloader Beats It)
If you’ve been writing Helm charts for a while, you’ve probably seen this pattern in pod templates:
spec: template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}This bakes a checksum of the ConfigMap into the pod template at Helm render time. When the ConfigMap content changes and you run helm upgrade, the checksum changes, the pod template changes, rolling restart happens.
It works. But notice that critical dependency: helm upgrade. If you edit the ConfigMap directly (kubectl edit, or an operator updates it, or a Secret is rotated by cert-manager), Helm doesn’t know and the pods don’t restart.
Reloader has no such dependency. It watches the live cluster state, not Helm’s view of it. Edit the resource any way you want — Reloader catches it.
The Helm checksum pattern is a fine baseline if you strictly manage everything through helm upgrade. Reloader is better if you live in the real world where things change in more ways than one.
Excluding Specific Environments
Maybe you want Reloader enabled everywhere except production, where you have a strict change window and you’ll restart pods on your own schedule, thank you very much.
The cleanest approach is the search/match pattern. In non-prod, annotate your ConfigMaps with reloader.stakater.com/match: "true". In production, simply don’t add that annotation. Deployments with search: "true" will still watch, but they won’t trigger unless a matching ConfigMap changes — and in production, nothing is annotated to match.
Alternatively, deploy Reloader only to your non-prod clusters. Since it’s namespace-scoped by default in its RBAC, you can also restrict which namespaces it watches at install time:
helm install reloader stakater/reloader \ --namespace reloader \ --create-namespace \ --set watchGlobally=false \ --set namespaceSelector='environment!=production'That namespaceSelector is a label selector — label your production namespace environment: production and Reloader leaves it alone.
Failure Modes: When a Restart Arrives at the Worst Time
Here’s a scenario that will ruin your afternoon: you have a StatefulSet (say, a Redis cluster) mid-rollout because you just scaled it up. At the same moment, someone rotates the TLS Secret it uses. Reloader sees the Secret change and patches the StatefulSet. Now you have two rolling changes fighting each other.
For StatefulSets especially, make sure you’re using podManagementPolicy: OrderedReady:
spec: podManagementPolicy: OrderedReady updateStrategy: type: RollingUpdateOrderedReady means Kubernetes won’t start terminating pod N until pod N+1 is ready. It’s slower, but it means concurrent restarts (one from Reloader, one from your manual scale) don’t pile up. With Parallel policy, you can end up with your entire StatefulSet restarting simultaneously. That’s a bad day.
Also: Secret rotation deserves extra care. If you’re using something like External Secrets Operator or cert-manager to rotate secrets automatically, every rotation is a potential Reloader trigger. Make sure that’s what you want before annotating a StatefulSet with auto: "true" and a fast-rotating Secret.
ArgoCD Compatibility (The Gotcha Everyone Hits)
This is the one that trips up anyone running GitOps with ArgoCD.
Reloader works by patching the Deployment’s pod template — it adds an annotation or env var with a checksum. But your ArgoCD Application has a desired state that comes from Git. The Reloader-injected annotation is not in Git. So ArgoCD sees a diff and either marks the app as OutOfSync, or worse, reverts the patch on the next sync (which means your pods restart again — back to old config).
Fix this with ignoreDifferences in your ArgoCD Application:
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: my-appspec: ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/template/metadata/annotations/reloader.stakater.com~1last-applied-configuration jqPathExpressions: - .spec.template.spec.containers[].env[] | select(.name | startswith("STAKATER_"))The ~1 is JSON Pointer encoding for a forward slash — so reloader.stakater.com~1last-applied-configuration means the annotation key reloader.stakater.com/last-applied-configuration.
The jqPathExpressions entry handles the env var injection. With both of these in place, ArgoCD treats Reloader’s patches as expected noise and stops reverting them.
Some teams also set syncPolicy.syncOptions to include RespectIgnoreDifferences: true to ensure the ignore rules are respected during automated sync. Worth adding if you’re running auto-sync:
spec: syncPolicy: syncOptions: - RespectIgnoreDifferences=trueShould You Bother?
If you’re running k3s or a single-node homelab cluster with three Deployments and you restart pods maybe once a week, the honest answer is: probably not. The manual kubectl rollout restart takes five seconds.
But if you have more than a handful of services, if you’re rotating secrets automatically (cert-manager, External Secrets), if you’ve ever shipped a config change and spent 20 minutes debugging why the app is still behaving wrong before realizing the pods never restarted — Reloader is a one-time install that pays for itself in the first week.
The search/match pattern makes it safe for mixed environments. The ArgoCD ignoreDifferences config makes it GitOps-compatible. The Helm install takes two minutes.
It’s one of those boring, reliable tools that just works in the background and makes you feel slightly less like you’re herding cats. In Kubernetes terms, that’s high praise.
Install it, annotate your Deployments, and stop babysitting your config refreshes.