Blog · May 2026 · 10 min read

Migrating an AI Agent from WSL to Docker on Unraid

My AI agent — Hermes — had been running in WSL2 for months. It worked, mostly. The problem was that WSL2 terminals would randomly die. Sometimes the entire distro would vanish overnight. The agent's brain, including its memory, cron jobs, and conversation state, lived in an environment that couldn't be trusted to stay running.

The Problem with WSL2 as Production

WSL2 is fantastic for development. It's a terrible place to run anything that needs to survive a reboot. The filesystem is ephemeral by default, the networking stack is NAT'd behind Windows, and the lifecycle is tied to whatever Windows decides to do with your VM.

For an AI agent that runs 24/7 — processing cron jobs, maintaining Discord bot connections, and keeping conversation state — this was a ticking time bomb. Every time WSL crashed, I'd lose whatever the agent was doing and have to manually restart everything.

The Target: Docker on Unraid

I have an Unraid server (a mini6900hx) already running Docker containers for LanceOS, BizExpenses, and other services. It's stable, has UPS backup, and its filesystem persists across reboots. Moving Hermes there was the obvious call.

The Phased Approach

I didn't want a big-bang migration. Too many things could go wrong. Instead, I broke it into gates:

  1. Gate A: Establish SSH access from the current environment to Unraid
  2. Gate B: Create the Unraid directory structure, capture inventory, seed source files
  3. Gate C: Build Docker image, write compose file, test health checks
  4. Gate D: Cutover — stop WSL services, start Unraid services, verify

Each gate had an explicit approval step. If something broke, I could roll back to the previous gate without losing anything.

SSH Key Setup

First, I generated a dedicated SSH key for the migration:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_unraid_root \
  -C "lhiggins@wsl-to-unraid-root"

Then installed it on Unraid with ssh-copy-id. This gave me passwordless SSH access as root, which I'd need for Docker operations. The key was stored in the Hermes secrets directory for persistence.

Seeding the Source

The Hermes home directory was about 3GB, but most of that was rebuildable (venv, node_modules, .git). I used rsync over SMB to seed the source, excluding the bulk:

rsync -a --info=progress2 --inplace \
  --exclude='.git' --exclude='venv' --exclude='node_modules' \
  --exclude='__pycache__' --exclude='*.pyc' \
  ~/.hermes/hermes-agent/ /mnt/unraid/hermes/source/hermes-agent/

The final seed was 386MB across 8,363 files. Much more manageable.

The Backup

Before touching anything, I took a compressed backup of the entire WSL Hermes home:

tar --zstd -cf hermes-wsl-pre-migration-20260518.tar.zst ~/.hermes/

The result: a 1.0GB archive with a SHA256 checksum, stored on Unraid. If everything went sideways, I could restore from this in minutes.

Docker Compose

The final compose file runs two Hermes profiles — a default gateway for the TUI/API, and a Discord-specific gateway. Both share the same home directory mount:

services:
  hermes-gateway:
    build: ./source/hermes-agent
    volumes:
      - /mnt/user/appdata/hermes/home:/home/lhiggins
    ports:
      - "9119:9119"
    restart: unless-stopped

  hermes-gateway-discord:
    build: ./source/hermes-agent
    volumes:
      - /mnt/user/appdata/hermes/home:/home/lhiggins
    restart: unless-stopped

Both containers mount the same persistent home directory, so memory, skills, and config are shared.

The 72-Hour Soak Test

After cutover, I didn't touch anything for 72 hours. I watched the logs, checked cron job execution, verified Discord bot uptime, and monitored memory usage. Everything held. The agent had been running for months on WSL with regular crashes. On Docker/Unraid, it's been up for weeks with zero incidents.

Lessons

Need help with this?

I consult on Docker migrations, home server setup, and AI agent infrastructure. Real systems, not theory.

Work with me →
← Back to blog