FutureGo
3 dk okumaEN

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.

Yazan: Yücel Yılmaz

CoolifyHermes AgentDockerDevOpsSelf-hosting

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.

The problem: the upstream compose file doesn't know about Coolify

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:

  • It defines its own networks and expects fixed names.
  • It binds ports directly to the host — which collides with Coolify's reverse-proxy model.
  • Some services rely on 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."

Fixing Traefik routing

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: true

Two things matter here:

  1. Drop the host port bindings. Otherwise both Traefik and the host try to own the same port, and behavior shifts depending on deploy order — one of the hardest bug classes to debug.
  2. The coolify network must be external: true. Letting Compose create its own network puts the service on an island Traefik can't reach.

Tunnel or public domain?

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.

Credential handling

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:

  • Secrets never touch the repo or a plaintext file on the server.
  • Coolify's redeploy flow carries secrets automatically.
  • Rotation happens from a single place.

What I learned

  • Don't fork, write an adaptation layer. Instead of editing the upstream compose file directly, writing a script that transforms it made tracking upstream updates much easier.
  • "The container is running" and "the service is reachable" are different claims. When something breaks in Coolify, the first place to look isn't the app logs — it's Traefik's service discovery state.
  • Defaults should be safe. Making tunnel mode the default makes "I just wanted to try it quickly and accidentally exposed it to the internet" impossible by construction.
  • Self-hosting isn't something you set up once, it's something you operate. Documenting reinstall and upgrade paths matters as much as the deployment script itself.

If you hit a similar wall deploying something on Coolify, feel free to open an issue on the repo.