I run Hermes as a self-hosted AI agent in a small KVM lab rather than installing it directly on my workstation. The VM is configured as a QEMU q35 guest with 8 GiB assigned memory. Inside the guest, Hermes and its supporting services run as a regular user under systemd --user.
This setup gives me a clean boundary for the agent, predictable restarts, and a simple way to inspect what is actually running. The important detail is to point systemd directly at the virtual environment entry point instead of using a shell wrapper.
1. Confirm that the host is a virtual machine
From inside the guest, I first confirm that I am troubleshooting the VM I expect:
systemd-detect-virt --vm
uname -srmo
On this VM the result is kvm, followed by the Linux kernel and architecture. This only proves the guest virtualization type; it does not show the complete QEMU device configuration. I check the VM definition from the KVM host when I need to verify the machine type, assigned RAM, disks, or devices.
2. Check the memory the guest really sees
Guest-visible memory is the number that matters for sizing services:
free -h
awk '/MemTotal|SwapTotal/ {print}' /proc/meminfo
My guest currently reports about 7.2 GiB of RAM and a 3.1 GiB swap device. The configured VM size is 8 GiB, but the guest will always report less than the assigned amount because firmware and virtual devices consume some memory.
This check also catches a common KVM surprise: a virtio-balloon device can make the guest see less memory than expected. If memory appears to be missing, check the QEMU/libvirt definition for a balloon device and compare it with MemTotal inside the guest before tuning applications.
3. Run the agent from its virtual environment
I keep Hermes in its own virtual environment:
ls -l /home/hermes/.hermes/hermes-agent/venv/bin/hermes
head -n 3 /home/hermes/.hermes/hermes-agent/venv/bin/hermes
The systemd unit should call that entry point directly. My dashboard unit uses:
ExecStart=/home/hermes/.hermes/hermes-agent/venv/bin/hermes dashboard --port 9119 --host 0.0.0.0 --no-open
I avoid pointing a systemd unit at ~/.local/bin/hermes. That file is a shell wrapper intended for an interactive shell and can have a different environment from the user service manager. Calling the venv entry point makes the interpreter and installed dependencies unambiguous.
4. Use systemd user services for the long-running components
For a single-user lab, systemd --user is enough. I can see the running services with:
systemctl --user --no-pager --type=service --state=running
The core services in my setup are the Hermes dashboard, the Hermes gateway, the Hermes web UI, and the Hindsight daemon and UI. To inspect the exact command and unit file for each service:
for u in hermes-dashboard.service hermes-gateway.service hermes-webui.service hindsight-daemon.service hindsight-ui.service; do
systemctl --user show "$u" \
-p Id -p ActiveState -p SubState -p ExecStart -p FragmentPath \
--no-pager
done
I want each service to show ActiveState=active and SubState=running. The FragmentPath output tells me which unit file is actually being used, while ExecStart removes ambiguity about which Python or Hermes executable launched the process.
5. Verify the processes, not just the unit state
A running systemd unit does not automatically mean the expected application is running. I also inspect the process command lines:
ps -eo pid,user,comm,args --no-headers \
| grep -E '(hermes|hindsight)' \
| grep -v grep
In my environment this shows the Hermes gateway running with the venv Python module, the dashboard using the venv Hermes executable, and the Hindsight processes using their intended environment. This is a useful check after an upgrade because a service can remain active while launching an old path or an unexpected interpreter.
6. Troubleshoot the user manager before restarting applications
When a user service does not start, I check the unit state and recent journal together:
systemctl --user status hermes-dashboard.service --no-pager
journalctl --user -u hermes-dashboard.service -n 100 --no-pager
If the service is enabled for boot but does not start after the VM reboots, I check whether the user manager is enabled to remain available without an interactive login:
loginctl show-user hermes -p Linger
loginctl show-user hermes -p State
I only change linger deliberately, because it controls whether the user service manager survives logout:
sudo loginctl enable-linger hermes
After changing a unit file, I reload the user manager and restart only the affected service:
systemctl --user daemon-reload
systemctl --user restart hermes-dashboard.service
systemctl --user status hermes-dashboard.service --no-pager
7. Keep the VM boundary simple
The useful pattern here is deliberately boring: a dedicated KVM guest, a dedicated Python environment, and user-level systemd units with explicit paths. I can verify the guest resources, the unit definitions, and the actual processes without depending on an interactive shell.
That makes upgrades easier to reason about. If something breaks, I can identify whether the problem is the VM resource definition, the Python environment, the systemd unit, or the application itself instead of treating “the agent is down” as one undifferentiated failure.