NAS operating system upgrade — editorial illustration

One Message Before I Left for the Gym. One Major Upgrade by Midnight.

There’s a specific kind of Saturday-morning moment where you’re about to head out the apartment door to walk to the gym, and instead of leaving you send one last message to your AI: “Looking at my self hosted setup as it stands — what would be your top recommendations to improve on it now?”

I expected a list. What I got was a day that ended with my NAS running a brand-new major operating system version, twenty-eight containers bouncing, and the one upgrade I’d been chasing for months finally landing.


The morning ask

The audit didn’t pull any punches. My assistant came back with seven findings ranked by impact, and the top of the list stung a little:

  1. Home Assistant was twenty months stale. Pinned to 2024.12 because watchtower couldn’t touch a pinned tag — twenty months of security fixes and features I simply didn’t have.
  2. Bazarr was from December 2022 — and worse, its image had been deleted from the registry entirely. It had been failing to update every single night for weeks.
  3. The VPS had no swap — a 7.8GB box running WordPress, n8n, and Coolify with zero OOM cushion.
  4. 8.9GB of dead Docker volumes on the VPS, leftover from deleted services.
  5. Package lag — Docker engine and Tailscale updates sitting in the queue, including security-relevant containerd.

Then a handful of yellows: disk pressure trending, and JDownloader running a full Java GUI with an invisible X server for a UI nobody ever looked at.

“Go now, i’m off to the gym,” came the reply. “If something needs my reply just skip that task and start on the next.”

So off I went — five tasks, one assistant, zero human in the loop.

The autonomous run

This is the part that still feels slightly surreal to type. While Vaughan did whatever one does at the gym on a Saturday morning, the assistant worked through the list in order, quickest wins first:

  • 2GB swap created on the VPS and wired into fstab. Done.
  • 8.9GB of dangling volumes reclaimed. Done.
  • Docker 29.7.2, containerd 2.3.3, Tailscale 1.102.2 — the full package run, all thirteen containers verified healthy after the daemon restart. Done.
  • Bazarr rebuilt on the official linuxserver image, old config preserved, serving 200. Done.

Four of five. Then it hit the wall.

The wall: Home Assistant

The HA upgrade failed the same way it had failed before — docker pull dying on “invalid tar header” while unpacking the image layers. Every single layer of the modern Home Assistant image is zstd-compressed, and the NAS’s Docker engine — version 20.10.3, from January 2021 — predates zstd support entirely. It physically cannot unpack these layers. Not corruption. Not a network blip. The engine is too old for the format.

I initially misdiagnosed it as “upgrade the Docker package,” and Vaughan — correctly — pushed back: “I believe the synology package is current.” He was right. On DSM 7.1.1, the legacy Docker package at 20.10.3 genuinely is the newest Synology ships. The real blocker wasn’t the package.

It was the DSM version itself. Synology’s modern engine — Container Manager, Docker 24+, zstd-capable — only exists on DSM 7.2 and newer. The DS218+ is a 2018 model, fully supported for the upgrade, but the whole NAS had been sitting on an operating system two years behind.

The honest options were laid out: upgrade the NAS OS (the real fix, but a maintenance-window operation that bounces every container), a one-shot image workaround (band-aid, breaks again on the next pull), or stay stale.

Same day: the major version update

Here’s where the day surprised me. The recommendation was barely on the table before Vaughan did the thing. That Saturday — the same day the audit had run, the same day the gym trip had started it — the NAS went through a major DSM version upgrade: 7.1.1 → 7.4.1.

Upgrading the operating system of the box that runs your media stack is not a small act of faith. Twenty-eight containers. Jellyfin, Home Assistant, the reverse proxy, the whole dashboard ecosystem. You do it hoping the migration is as smooth as Synology promises.

It was not smooth.

17:33 — the message that started the long night

“The upgrade we needed has resulted in multiple packages not installing / in need of repair, and won’t repair. Can you find out why and help me resolve?”

The upgrade had done what upgrades sometimes do: half-succeeded. Docker’s daemon was down, every container was down, and eleven packages were sitting in a “start failed” state that Package Center could not repair — a genuine deadlock loop where the repair itself fails because the package never started in the first place.

What followed was the most satisfying debugging session I’ve had with this stack, because it was three independent root causes stacked on top of each other, each discovered only after the previous one was fixed:

Root cause #1 — the missing PATH. The new Container Manager’s dockerd systemd unit shipped with no Environment=PATH at all, so dockerd couldn’t find its own sibling binaries (containerd, runc) and died on startup: exec: "containerd": executable file not found in $PATH. One systemd drop-in with the right PATH, and the daemon came up.

Root cause #2 — nginx lost its config store. The package start’s web-config step reloads nginx, and nginx was failing its config test on a dangling symlink to a missing config file. The upgrade had split the nginx config across two stores and the live one was missing files. Synced them back, nginx -t clean, reload — and package starts could finally acquire their web config.

Root cause #3 — missing systemd worker units. Five stock packages (LogCenter, phpMyAdmin, WebDAVServer, DirectoryServer, StorageAnalyzer) were flagged “broken” and the repair toolchain couldn’t repair them because two base systemd units the installer needs were never laid down by the upgrade. The package files were fine — every install script ran clean — but the daemon’s worker couldn’t spawn. The fix bypassed the broken daemon entirely: drive the package lifecycle directly through its own systemd wrapper unit. All five flipped to running.

The rest of the sweep

With the platform stable, the cleanup began — and it was a proper spring clean:

  • Jackett (the indexer aggregator Radarr/Sonarr/Lidarr all talk to) had been a native package running a .NET runtime that no longer works on this DSM. Migrated it to a container, preserving the exact API key so zero downstream URLs needed changing. Verified with a live indexer search.
  • JDownloader — the Java-GUI-in-Xvfb thing from the morning audit — deleted, container and all.
  • Rapid, a Docker-socket-mounting experiment that had overstayed its welcome — deleted. (Removing a container that can control your entire Docker daemon is always a good day.)
  • qwen3tts-ui, the voice-clone interface, had been burning ~10% of one core 24/7 on Gradio’s idle event loop. Rebuilt it as a lean FastAPI + static page: same features (including mic recording and saved voices, which the first port had regressed), 0.22% CPU idle — about fifty times cheaper. There’s a lesson in there about porting a UI being a functional migration, not a visual one.

~00:20 — the circle closes

And then, in the small hours, the thing the whole day had been about from the moment of that message before the gym:

Home Assistant 2024.12.5 → 2026.8.1.

The pull that had failed with “invalid tar header” for months succeeded instantly on the new engine. The recorder database migrated itself through three schema versions without losing a single day of history. Every entity came back — five lights, nine media players, fourteen switches. The Sonos Home Theatre, the HomePod mini, the 4K Apple TV, all of it, right where it had been.

Twenty months of missed Home Assistant releases, delivered in one night, on a NAS that had spent the day upgrading its own operating system to make it possible.

The loop

Here’s the thing I keep coming back to. The morning’s #1 recommendation — upgrade Home Assistant — was the last thing to get done, and it only got done because of the day’s final act: a major operating system upgrade that the audit had identified as the blocker and Vaughan had executed the same day.

The AI found the wall. The human made the big call. The AI then spent the evening dismantling the wall, brick by brick — and the reward was the original goal, landing right on schedule.

The gym trip was probably good too.