Deploying Hermes Agent on Coolify: Lessons Learned
Fixing Traefik routing, choosing between tunnel and public deployment modes, and building a sane credential model for a self-hosted AI agent on Coolify.
Published by: FutureGo Studio
Fixing Traefik routing, choosing between tunnel and public deployment modes, and building a sane credential model for a self-hosted AI agent on Coolify.
Published by: FutureGo Studio
I wanted to run NousResearch's Hermes Agent on my own VPS, managed through Coolify. My assumption was simple: "there's a docker-compose file, I hand it to Coolify, done." It wasn't done. This post walks through what actually broke and how I fixed it; the resulting script and guide live as open source at github.com/yucel-yilmaz/hermes-agent-coolify-deploy.
Coolify deploys a Docker Compose project with its own assumptions baked in: it wires services into a network it creates itself, discovers what should be exposed through Traefik labels, and expects environment variables to be managed from its own UI.
Hermes Agent's upstream compose file was written to run standalone:
depends_on ordering and healthchecks that, combined with how Coolify restarts containers, produce race conditions.On the first attempt, the containers came up fine, but Traefik couldn't see any of them: from the outside, no site; from the inside, everything "working."
The correct way to expose a service in Coolify isn't binding a port — it's handing the service Traefik labels and attaching it to Coolify's proxy network. The core change to the compose file boils down to this:
services:
hermes-agent:
# no host port binding: NO "- 8080:8080"
networks:
- coolify
labels:
- traefik.enable=true
- traefik.http.routers.hermes.rule=Host(`hermes.example.dev`)
- traefik.http.services.hermes.loadbalancer.server.port=8080
networks:
coolify:
external: trueTwo things matter here:
coolify network must be external: true. Letting Compose create its own network puts the service on an island Traefik can't reach.Exposing an AI agent to the internet shouldn't default to "anyone can reach it." I defined two modes:
Tunnel mode: the service is never attached to a public domain; access happens only over an SSH tunnel or VPN. If I'm the only consumer, this is the right choice — the attack surface is close to zero.
Public mode: the service sits behind Traefik on a real domain, with TLS managed by Coolify. In this mode, an authentication layer is mandatory — leaving the agent endpoint bare turns your VPS into someone else's LLM bill.
The script supports both and defaults to tunnel mode. I think the safe option should be the default, not an opt-in.
The upstream configuration reads API keys from a .env file. Coolify, on the other hand, defines secrets through its UI and injects them into the container as environment variables. The trap here: Coolify's handling of the env_file directive in the compose file may not match the file you expect.
My fix was to drop the env_file dependency entirely and move all secrets into Coolify's environment management. That gets you:
If you hit a similar wall deploying something on Coolify, feel free to open an issue on the repo.