homelab-agent
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 8 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Turn a €70 used mini PC into an always-on AI agent server: Hermes, n8n, Pi-hole, monitoring, Tailscale and encrypted backups, all as code.
homelab-agent
Turn a cheap used mini PC into an always-on AI agent server. The reference box is a €70
Shuttle DS61 (Intel i5-3475S, 8 GB RAM) from a classifieds site.
The agent's model runs in the cloud (your OpenAI/Anthropic/OpenRouter account, or a local model
if you have the hardware). The box hosts the agent, its memory, its tools and everything around
it, 24/7.
Every service except Caddy is optional (see Modules):
| Service | Address | What it does |
|---|---|---|
| Hermes Agent | hermes.home.arpa, <server>:9119 |
The agent. Chat with it on Telegram/WhatsApp, let it code and push to GitHub |
| n8n | n8n.home.arpa |
Workflow automation; can call Hermes' OpenAI-compatible API |
| Pi-hole | pihole.home.arpa |
DNS with ad and tracker blocking; also serves the *.home.arpa names |
| Uptime Kuma | status.home.arpa |
Uptime checks and alerts |
| Beszel | server.home.arpa |
CPU, RAM, disk, temperatures, fans, per-container stats |
| Caddy | port 80 | Gives every service a name; landing page at http://<name>.local |
| Tailscale | — | Reach everything from anywhere, no open ports |
| restic | — | Nightly encrypted backups, pulled to your Mac |
Everything is code: a fresh Ubuntu Server becomes this setup with one command, and a dead box
is rebuilt from the repo plus the latest backup.
What you need
- Any x86-64 PC with 8 GB RAM and an SSD. Tested on Ubuntu Server 26.04.
- A computer to set it up from (macOS or Linux), or a keyboard and screen on the server itself.
The backup pull inmac/is macOS-only. - A free Tailscale account.
- An LLM provider account for Hermes.
Set up
- Install Ubuntu Server on the box, with OpenSSH (the installer offers it).
- On your computer:
It asks for the server's address, a name and which services you want; everything else itgit clone https://github.com/sjespersen/homelab-agent && cd homelab-agent ./setup.sh
reads from the server. Then it copies your SSH key, installs Docker, the firewall, Tailscale
and the services, and starts Hermes' own setup (model, login, chat apps). You type the
server password once, the sudo password once, and open the Tailscale login link it prints. - Do the few things it lists at the end: reserve the server's IP in your router, create the
owner accounts in the web UIs, add the server as DNS server in the
Tailscale admin console (with Pi-hole), and save
the backup password in your password manager.
No second computer? Clone the repo on the server and run ./setup.sh --local; it can import
your SSH keys from GitHub.
setup.sh is safe to re-run: it skips what's done, resumes after an error, and applies
changed answers. Total time: about an hour, most of it waiting for downloads.
cp config.example.env config.envand fill it in;ssh-copy-id <user>@<server>../deploy.sh(copies the repo to~/homelab-agent; the stacks fail until Docker exists).- On the server, in a real terminal:
sudo bash ~/homelab-agent/bootstrap.sh. ./deploy.shagain.ssh -t <server> 'cd /opt/stacks/hermes && docker compose run --rm -it gateway setup',
then./deploy.shonce more.- On a Mac:
mac/setup-mac.shstarts the daily backup pull.
Modules
Pick the services in config.env:
MODULES="pihole hermes n8n uptime-kuma beszel" # the default: everything
MODULES="hermes" # just the agent
Caddy, the firewall, Tailscale and backups are always on. Turning a module off stops its
containers and closes its ports; its data stays in /opt/stacks/<module> (and in the backups).
Without Pi-hole nobody hands out the *.home.arpa names, so Caddy serves each service on its
own port instead: Hermes :8001, n8n :8002, Uptime Kuma :8003, Beszel :8004, all linked
from http://<name>.local.
A module is a folder in stacks/:
| File | What it's for |
|---|---|
compose.yaml |
The containers. Reads ${TZ}, ${DOMAIN}, ${PUID}, ${SITE_URL} and friends |
module.env |
Title, start order, name/port behind Caddy, firewall ports, containers to stop for backups |
site.caddy |
What Caddy does for its name or port (usually one reverse_proxy line) |
compose.with-<module>.yaml |
Added only when that other module is on (n8n joins Hermes' network) |
backup-excludes.txt, after-up.sh, root-setup.sh |
Optional: caches to skip, a step after start, host setup as root |
To add your own service, copy a module like uptime-kuma, give it a free PORT, and run./setup.sh.
Change something
Edit a compose file in stacks/, then ./deploy.sh. To add or remove services, run./setup.sh again (it also updates the firewall, which needs sudo).
See what's running, where, and when the last backup ran: ./deploy.sh --status.
Update all images about once a month: ./deploy.sh --update. It takes a backup first, so you
can roll back with restic if a new version breaks something.
Security
This is a home lab, not a hardened production setup. The defaults keep everything off the
internet, but anyone or anything that gets inside (onto your LAN, your tailnet, or into the
agent) gets a lot. Read this before you put anything valuable on the box.
What the defaults protect
- SSH accepts keys only, and only from
TRUSTED_NETSand Tailscale; passwords and root login
are off. - ufw blocks DNS, the web UIs and the Hermes ports from everywhere except
TRUSTED_NETSand
Tailscale. That includes the ports Docker publishes, which normally skip ufw (bootstrap.sh
adds matching rules to Docker'sDOCKER-USERchain).
Pi-hole, n8n, Uptime Kuma and Beszel listen on127.0.0.1only; outside the box they are
reachable only through Caddy. Without Pi-hole, Caddy's ports for them (8001–8004) get the
sameTRUSTED_NETSand Tailscale limits. - Only the modules you enable run, and only their ports are open.
- Hermes and n8n, the two containers that run code from outside, sit on Docker bridge networks.
They can't reach the other services' ports or Caddy (ufw drops traffic from Docker networks). - The Beszel agent talks to Docker through a read-only proxy, not the Docker socket.
- n8n's Execute Command and Local File Trigger nodes are off.
- Hermes pushes to GitHub with per-repo deploy keys, not your account.
- The Mac's backup key can only read the backup folder (
rrsync -ro). - Backups are encrypted with restic.
Attack vectors, most serious first
1. Prompt injection into the agent. Hermes reads web pages, emails, chat messages, issues
and code, and any of those can contain instructions written for it. It can run shell commands
in its container, and that container holds your LLM credentials, chat-app sessions (a stolen
WhatsApp session can read and send as you), API keys and GitHub deploy keys.
Reduce it: give it only the keys it needs; keep the chat apps on an allowlist of your own
accounts; don't connect it to inboxes or accounts you can't afford to leak.
2. Partial isolation between containers. Hermes and n8n are on bridge networks, but when
both are enabled they share one (so workflows can call http://hermes:8642/v1): a compromised
Hermes can reach n8n's login and webhooks, and the other way round. Pi-hole, Caddy, Uptime Kuma and Beszel still use
host networking and can reach each other's 127.0.0.1 ports, including the read-only Docker
proxy on port 2375 (see 3).
Reduce it: use strong, unique passwords for every web UI, and authentication on n8n
webhooks.
3. Several paths to root. Anything that controls Docker controls the host:
- Your user is in the
dockergroup, so your SSH key is effectively a root key, and the sudo
password doesn't protect anything. - The socket proxy for Beszel mounts the Docker socket. It only passes on reads (container
list, inspect, stats, logs; server info), so a compromised Beszel agent can't start
containers. The proxy image itself is trusted with root. The reads still expose every
container's environment variables and logs to anything on the host network (port 2375 on127.0.0.1), so keep secrets out of composeenvironment:(Hermes and n8n keep theirs indata/).
Reduce it: protect your SSH key with a passphrase; remove thesocket-proxyservice andDOCKER_HOSTfromstacks/beszel/compose.yamlif you don't need per-container stats.
4. Ransomware can reach the backups. Anyone who controls the server can delete or encrypt~/backups/restic (the password sits next to it in ~/.config/restic/password). The Mac
pulls with rsync --delete, so the next pull mirrors that damage onto the Mac copy.
Reduce it: keep Time Machine (or any versioned backup) running on the Mac so older copies
survive; check restic snapshots now and then.
5. Plain HTTP on the LAN. Logins for Hermes (basic auth), n8n, Uptime Kuma, Beszel and
Pi-hole travel unencrypted when you use them over the LAN. Anyone on the same network can
capture them: a guest on your Wi-Fi, a compromised smart TV or IoT device. TRUSTED_NETS
usually means "your whole LAN".
Reduce it: use the services over Tailscale (encrypted) even at home, and narrowTRUSTED_NETS or drop the LAN entries entirely. Put IoT and guest devices on a guest network.
6. Everyone on your tailnet gets everything. ufw allows all traffic on tailscale0. Any
device you add, any node you share with someone, and anyone who takes over your Tailscale
account can reach every port on the box.
Reduce it: turn on 2FA for the account behind Tailscale; use
Tailscale ACLs to limit who reaches the server; remove
old devices.
7. Unpinned, unupdated images. Most images use :latest, so you trust whatever the
maintainers publish next. --pull missing never updates them on its own, so known holes stay
until you update. Ubuntu installs its own security updates (unattended-upgrades); containers
don't.
Reduce it: update monthly with ./deploy.sh --update (takes a backup first, then pulls
newer images for every stack); pin versions where you care; follow the release notes for
Hermes and n8n.
8. Secrets at rest. Ubuntu's default install has no disk encryption, so whoever takes the
box gets every key and session on it. Secrets also sit in plain files under /opt/stacks
(Hermes data/.env and auth.json, n8n's encryption key) and in every backup.
Reduce it: choose disk encryption when you install Ubuntu (you then type the passphrase at
every boot); keep the box somewhere it won't walk off.
Smaller items
- Hermes' API on port 8642 gives full agent access to anyone with the key on
TRUSTED_NETSor
the tailnet. Treat that key like a password. - n8n webhooks are open by design to anyone who can reach n8n. Add authentication in the
webhook node. - Tailscale is installed with
curl | sh, andbootstrap.shruns as root: read scripts before
you run them, including these. ./setup.sh --localcan import your SSH keys from GitHub: whoever controls that GitHub account
then has a key to the server. Remove keys you don't use from~/.ssh/authorized_keys.- The Mac pulls with rsync from the server; rsync clients have had bugs a malicious server
could exploit. Keep Homebrew's rsync updated. - Avahi announces the server's name and services on the LAN.
- If your ISP changes your IPv6 prefix, update
TRUSTED_NETSand re-runbootstrap.sh. Until
then the server is reachable over IPv4 on the LAN and over Tailscale, including SSH. - If you add a rule to
/etc/ufw/after.rulesyourself, keep it outside thehomelab-agent DOCKER-USERblock;bootstrap.shrewrites that block.
Backups
- Every night at
BACKUP_TIMEthe server stops the containers that use SQLite (~15 s), takes
a restic snapshot of/opt/stacksinto~/backups/restic, and keeps 7 daily, 4 weekly and
6 monthly snapshots. Caches are excluded (each module'sbackup-excludes.txt). - The Mac copies that repo to
~/Backups/<name>/resticonce a day while awake. Its SSH key is
limited to read-only rsync of that one folder, so a stolen Mac can't touch the server. - The repo password is in
~/.config/restic/passwordon the server and in the Mac Keychain
(item<name>-restic). Save it in a password manager. Without it the backups can't be opened.
Check them:
./deploy.sh --status # next run, latest snapshot, last Mac pull
RESTIC_PASSWORD_COMMAND='security find-generic-password -s <name>-restic -w' \
restic -r ~/Backups/<name>/restic snapshots
Rebuild from scratch
- Install Ubuntu and run
./setup.shwith your existingconfig.env; skip Hermes' setup. - Stop the containers, then restore the app data from the Mac copy:
restic -r ~/Backups/<name>/restic restore latest --target /tmp/restore rsync -a /tmp/restore/opt/stacks/ <user>@<server>:/opt/stacks/ ./deploy.sh. On the Mac,mac/setup-mac.shre-authorises the pull key (setup.sh
already did if you ran it from the Mac).- If the Tailscale IP changed: update the Tailscale DNS settings (and
TAILSCALE_IPinconfig.env, if you set it).
Let Hermes code and push to GitHub
Hermes works in /opt/data/workspace inside its container (/opt/stacks/hermes/data/workspace
on the host). Give each repo its own deploy key, so Hermes can only push to that one repo:
- On the server:
ssh-keygen -t ed25519 -N '' -C hermes -f /opt/stacks/hermes/data/.ssh/myrepo_deploy - On GitHub: the repo → Settings → Deploy keys → add the
.pubfile, tick Allow write access. - Clone it inside the container and make the key stick:
docker exec -u $(id -u) -w /opt/data/workspace hermes sh -c ' export GIT_SSH_COMMAND="ssh -i /opt/data/.ssh/myrepo_deploy -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" git clone [email protected]:you/myrepo.git && cd myrepo && git config core.sshCommand "$GIT_SSH_COMMAND" && git config user.name Hermes && git config user.email [email protected]' - Recommended: add a
.git/hooks/pre-pushthat runs your lint, tests and build, so a broken
commit never leaves the box. - Tell Hermes about the repo and your rules (branch or straight to
main, what to test).
It saves them in its memory.
Revoke access any time by deleting the deploy key on GitHub.
Extras
extras/it87-fan/: makes the fan quieter on boards whose BIOS sets a loud minimum fan speed
(many ITE IT87xx Super I/O chips). See its README.
Not covered
Wi-Fi/ethernet config (netplan), hostname, and router settings: they depend on your hardware
and network. Local models: an 8 GB box without a GPU can't run useful ones; point Hermes at a
cloud provider or at a bigger machine on your tailnet.
License
MIT
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found