Patch management is supposed to be boring. You want a list of machines, a clear view of missing updates and a reliable way to keep everything under control. The less drama around it, the better.
My PatchMon instance had been running for a while on a dedicated Docker server. It did its job, but it was also one more special machine to look after. When I moved more of the infrastructure into Kubernetes, PatchMon was an obvious candidate to follow. The broader platform migration is described in my earlier Docker Swarm to Talos Linux post.
The goal was simple: move the application without losing the data, the 36 monitored machines or the integrations built around it. In other words, make the migration invisible to the machines reporting to it and to the operators checking their status.

Starting with a safety net
I did not begin by stopping containers and hoping for the best. First, I took a pre-cutover PostgreSQL dump, then another final dump once the source instance was ready to freeze. The old Docker stack was kept available throughout the transition so that going back remained a real option.
The database was restored into a dedicated PatchMon database on my existing PostgreSQL service. Redis and guacd were prepared on the Kubernetes side, and the application secrets were handled through SOPS. Nothing particularly glamorousbut these are the details that decide whether a migration is calm or stressful.
A small, predictable Kubernetes deployment
PatchMon now lives in its own namespace and is managed through GitOps. The manifests describe the application, its Redis state, guacd and the network entry point. Flux applies the changes, and GitLab keeps a history of what changed and when.
I deliberately kept the architecture straightforward. PostgreSQL remains a database service, Redis handles the fast-moving application state, and Kubernetes runs the application itself. There is no need to turn a relatively small monitoring tool into a science project.
Keeping the URL unchanged
There are no public users for this service. PatchMon is an internal endpoint used by my infrastructure, so the important thing was to keep the machines pointed at a stable address while moving the backend.
HAProxy was pointed at the new Kubernetes service, with the configuration checked before the reload. Internal DNS was updated and HTTPS continued to use the existing wildcard certificate. From the machines’ point of view, the endpoint had not changed.
Moving the agents
Once the new instance was responding correctly, I moved the agents one by one to the internal HTTPS endpoint. Each machine reports its update state to PatchMon and can receive patch actions from it, so each configuration was backed up before the change, the service was restarted, and the next report was checked.
Most of the machines were straightforward. A few did not have a direct SSH path, so I used the Proxmox QEMU Guest Agent to update their configuration. It is a small operational detail, but it is exactly the sort of detail that tends to be forgotten when a migration plan only considers the easy servers.
The final check showed all 36 machines active with fresh reports. The old endpoint remained available through a temporary legacy proxy while I completed the validation.
Turning off the old server
After checking the dashboard, the API, the agents and the Homepage integration, I stopped the remaining legacy proxy. The old Docker containers were kept in place but disabled, so the rollback path was still there if I needed it.
Only after that did I shut down the legacy update server cleanly. The old port was closed, the Kubernetes pods remained ready, and the health endpoint continued to return HTTP 200.
The useful part: fewer exceptions
PatchMon is now maintained like the rest of the Kubernetes workloads. Updates go through GitOps, database backups are taken before application changes, and the validation steps are repeatable. The project itself is documented on the official PatchMon website.
Homepage also uses the Kubernetes service directly for its PatchMon statistics. There is no public dashboard here; the backend call stays inside the cluster and the metrics remain part of the internal operations view. It gives me the useful numbers at a glance: how many machines are monitored, how many need updates and how many have security updates waiting.
What I learned
- Preserve the URL whenever possible. It removes a surprising amount of friction.
- Move the agents only after the new API is proven healthy.
- Keep the old stack available until the last meaningful check is complete.
- Backups are not a ceremonial step; they are what makes a rollback credible.
- The awkward servers matter. Plan for the machines without a convenient SSH path.
The migration was successful because the final result was intentionally uneventful: the data was still there, all 36 machines were reporting, the integrations worked and the old server could finally be switched off without requiring any change to the machines’ reporting or patch workflow.
Boring, but loving it (: