Skip to content

KTM Joins Inventory: From Custom 0.5.81 to Vendor 0.5.86

Adityo Guni Waluyo

KTM was invisible to Ansible until I added it to two inventory groups. One playbook run later, it swapped from custom 0.5.81 to vendor 0.5.86 with data intact.

TL;DR

Ansible skipped KTM because the host wasn't in inventory, so --limit couldn't target it until two group edits added it. The playbook then upgraded the container from a custom 0-5-81 build to vendor latest 0-5-86, now back in sync. Dashboard password persisted via bind-mounted datadir, while the old custom image remains on disk for later cleanup.

I ran ansible-playbook upgrade-9router.yml --limit KTM expecting KTM to pull the new image, and Ansible just skipped it. No error, no failed task, just zero hosts matched.

That was the moment I knew onboarding had not actually happened yet.

My Playbook Is Not Broken

My first guess was wrong. I assumed the playbook logic was off or I had a typo in the --limit flag. I re-checked the play role, re-ran with -v, still nothing. I even thought about hardcoding the host into the play for a one-off run.

Turns out that is not how Ansible works. Patterns depend on inventory: a host not listed in the inventory cannot be targeted by any pattern, and --limit does not invent a host, it only filters the inventory you already have, per the Ansible patterns documentation. KTM existed as a server, but as far as Ansible was concerned, it did not exist.

So the fix was not in the playbook at all. It was two inventory edits. I added KTM to the 9router and 9router_data_root groups in ansible/inventory/hosts.yaml, and added the 9router tag to the KTM block in inventory.yaml. Two files, a few lines. Until a server is a member of the groups a playbook targets, it is not managed. That is my firm rule now: if it is not in inventory, it is not managed, no matter if you can SSH to it.

Once that was done, the same command worked. upgrade-9router.yml --limit KTM finally targeted one host and did its job.

The Vendor Finally Caught Up

The job itself was a swap I had been waiting for. On 2026-09-20 I had built 9router-custom:0.5.81 from source because the vendor registry was stuck. Docker Hub decolua/9router tag latest was still at 0.5.75 from 2026-09-10, per the Docker Hub tags page, so there was no vendor image to pull.

Twelve days and six versions later, that lag closed. Hub latest was pushed on 2026-09-22 and now points to 0.5.86, which matches the newest release on the npm registry. The playbook was never broken. The registry was just late.

Running the playbook with KTM now in inventory moved the container from 9router-custom:0.5.81 to decolua/9router:latest, which is 0.5.86. The health check came back HTTP 307 with hasUpdate:false. That hasUpdate:false is the signal I wanted: we are back on the vendor track, no drift.

Short version: the custom image was a temporary bridge, not a fork. I would rather track latest from the vendor than maintain a custom build forever.

The Password Survived for a Reason

I was a little nervous about the dashboard password. But it survived the recreation without me re-seeding it.

That is because the hash lives in the bind-mounted datadir /root/.9router. A host directory mounted into the container keeps app state alive across container recreation, as documented in the Docker bind mounts guide. The playbook could swap the image completely and the password hash was still there on the next start.

One loose end remains. The old 9router-custom:0.5.81 image still sits on KTM's disk, unreferenced. It is a candidate for docker image rm on the next disk cleanup pass. That command is local-only anyway and never removes images from the registry, per the docker image rm reference; if a container were still using the image you would need -f. Low priority, but I noted it so it does not sit there forever.

Onboarding, for me, is now a checklist I can reuse: two inventory edits, one --limit playbook run, one endpoint verification. If all three pass, the server is managed.

Related articles