Adding Langfuse Observability to Hermes Agent (Self-Hosted, the Hard Parts Included)
How do you add observability to Hermes Agent with self-hosted Langfuse? A walkthrough of enabling the bundled plugin, the SDK gotcha that silently kills tracing, per-model cost tracking on Coolify, and what the traces actually look like. Langfuse observability for Hermes Agent is the way to see, per query, what your self-hosted agent actually does: which model it called, how many tokens it burned and what it cost. Hermes Agent is not built on LangChain, so it reports traces through Langfuse’s native OpenTelemetry support instead of a framework plugin. In this post I wire self-hosted Langfuse into a self-hosted Hermes Agent on a single Hetzner VPS, and share the gotchas that only show up when you actually run it. My Hermes agent runs 24/7 on my own VPS. It answers me on Telegram, fires cron jobs at 3 AM and swaps between half a dozen models depending on the task. Here is the embarrassing part: I had no idea if it was costing me ₹50 a month or ₹5,000. For someone who works in AI observability, being blind to my own agent’s spend felt a bit like a dentist walking around with a toothache. I use the OpenCode provider for Hermes. My default profile runs on I have 15+ cron jobs running round the clock. Some fire only in the dead of night between 2 and 5 AM, some every 30 minutes, some only on Sundays. I wanted to know which of these were quietly eating the most tokens, and which tools the agent kept reaching for and how often. This is exactly the visibility Langfuse gives me. My whole setup sits on one Hetzner CX43 VPS (8 vCPU, 16GB RAM, 80GB NVMe) running Coolify to manage containers. Hermes runs in a Docker container, and Langfuse is self-hosted right next to it on the same box. That means I run the observability stack myself instead of shipping my traces to a third-party SaaS dashboard, and I don’t pay a rupee for it. To be precise: my LLM calls still go out to the model providers and Langfuse offloads trace blobs to Cloudflare R2, so this is about keeping control of the Langfuse deployment, not pretending that no byte ever leaves the box. The diagram below shows how the pieces fit together. Though, Coolify has a one-click install for Langfuse, It did not work for me due to S3 dependency. So, I had to tweak the docker-compose.yml file to remove the S3 dependency, still it did not work. Finally, I used Cloudflare R2 as blob storage by setting the following env vars in the Langfuse container: Rest all env vars can be set as per the Langfuse documentation. After setting these env vars, I was able to successfully run the Langfuse container. You can create a new organization in Langfuse and get the public key, secret key and base URL for your self-hosted Langfuse instance. These will be used in the next step to enable the Langfuse plugin in Hermes Agent. Let’s move our attention to the Hermes Agent. We need to enable the Langfuse plugin in the Hermes Agent. You can do this by running the following command in the terminal inside the Coolify container or from your Chat interface of Hermes Agent (Telegram for me). This command enables the Langfuse observability plugin for the Hermes Agent. In Telegram you can directly ask it in natural language like “Enable Langfuse plugin” and it will enable the plugin for you. You can verify that the plugin is enabled by running: One thing the enable command does not do is install the Langfuse Python SDK. On a clean Hermes install the plugin imports The obvious move is a quick Point the Coolify service at this image and the SDK is there for good. Either way, prove it survives a redeploy, not just the first start. If the langfuse SDK is not importable by the gateway’s Python, the plugin loads as a dead no-op and you will chase missing traces for hours. Install the SDK durably, then confirm it is still importable after a Coolify redeploy, not only on first boot. This is the most important section of this post. If you set this up correctly 90% of the work will be done. These are the three variables the bundled Hermes plugin itself uses: Those three are the whole configuration the bundled plugin needs. It builds the Langfuse client straight from these values and creates its own exporter from the base URL and credentials. There are no extra transport variables to set for the plugin itself. So when traces did not show up for me at first, reaching for more variables was the wrong instinct. An empty dashboard is almost always upstream of configuration: the The langfuse SDK and the enabled plugin are what instrument Hermes and create spans (see Section 4). If the SDK is missing, the plugin hooks are inert and no environment variable will produce a single trace. Check the SDK, the gateway restart and the base URL before anything else. While I was flailing around trying to get traces, I also had these OpenTelemetry variables set on my container, and I cannot cleanly say which change finally flipped tracing on: Be honest with yourself here, because I was not at first: the bundled Hermes plugin does not read these variables. It configures Langfuse from the three If you do use that generic OTLP path, mind the endpoint: behind a normal Coolify HTTPS proxy, use your public base URL with no port ( Other than these, there are optional env vars that can be set: I did not set these optional env vars. Here is the explanation of the env vars. After setting the environment variables, you can verify that they reached the process by checking the You can also confirm the variables are set in the Coolify container settings for the Hermes Agent. Remember to restart the gateway after changing env vars with Do not ask Hermes on Telegram (or any chat surface) to print your environment variables. That drops HERMES_LANGFUSE_SECRET_KEY, and any other provider or bot credentials it can see, straight into your chat history and logs where they live forever. Check that a variable is present, never its value. Ask a few questions to the Hermes Agent and you will see the traces in the Langfuse dashboard. You can see the multi-turn session, tool-call spans, and token/cost breakdown across providers. Here are a few screenshots of the Langfuse dashboard showing the traces of the Hermes Agent: Langfuse out of the box provides three dashboards: Cost, Usage and Latency. You can see the cost dashboard below: This is the part I actually cared about. Setting up the dashboards is plumbing. The payoff is the answer to the question I started with: where is my money going? After letting the traces pile up for a week, here is what I found. The cron flood: I already keep a regular eye on my cron jobs, but the traces told a different story. They are absolutely flooded with cron runs. Clearly I need to dial down how often some of them fire. Too many granular tool calls: The agent was processing each comment on a GitHub issue as a separate step. More comments meant more LLM calls, which meant more cost. A single issue turned 16 terminal tool calls into 10 LLM calls. I need to batch them so the model runs once, not once per comment. The blind spot I still cannot explain: The OpenCode usage graph shows the The moment my monthly limit resets, I will remove the wasteful cron jobs and cut the frequency of the ones burning the most tokens and tool calls. That one change alone should save me a good chunk. This is one of the reason to set Observability for your agents. As an AI Observability Architect by profession, I know setting up the observability tools are just the beginning. The real value add comes from analyzing the traces and making improvements to the agent. However, I also know that observability tools can generate a lot of data and can become an ops problem if not managed properly. For example, I will set the Yes. Hermes ships a bundled Langfuse plugin that you enable and configure with the three This was my exact problem, and the fix was not more variables. The bundled plugin reads only the three Yes. My Hermes profiles run on You can use Langfuse Cloud, but I self-host it on the same Hetzner VPS as Hermes. That keeps all my agent data private and costs nothing beyond the server I already run. The tradeoff is you own the ops: storage, disk growth and retention are now your problem. More than I expected. On my setup it grew at roughly 1.5GB per day and was one of the main culprits filling up my VPS. Plan for sampling with It expects S3-style blob storage and would not start out of the box. I fixed it by pointing it at Cloudflare R2 using the 
deepseek-v4-flash, which is cheap per token. But I also keep separate profiles like legal and coding that use pricier models like GLM-5.2, Minimax and DeepSeek-V4-Pro. OpenCode gives me usage per model, but never per query. To close that gap, I plugged Hermes into Langfuse.1. Why observe a personal agent at all
2. The stack
flowchart LR
User["Me
(Telegram, CLI, cron jobs)"] --> Hermes
subgraph VPS["Hetzner CX43 VPS (Coolify)"]
direction TB
Hermes["Hermes Agent
(Docker container)"]
Langfuse["Langfuse
(self-hosted)"]
Hermes -- "OTLP traces
(OpenTelemetry)" --> Langfuse
end
Hermes -- "LLM calls" --> Providers["Model providers via OpenCode
(DeepSeek, GLM, ...)"]
Langfuse -- "blob storage" --> R2["Cloudflare R2"]
3. Setting up Langfuse
LANGFUSE_S3_EVENT_UPLOAD_BUCKET, your bucket name in R2LANGFUSE_S3_EVENT_UPLOAD_REGION, your R2 region. I have set it as auto.LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID, your R2 access keyLANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY, your R2 secret keyLANGFUSE_S3_EVENT_UPLOAD_ENDPOINT, your R2 endpoint. I have set it as https://<account_id>.r2.cloudflarestorage.com. Replace <account_id> with your Cloudflare account ID.

4. Enabling the plugin

hermes plugins enable observability/langfuse
hermes plugins list
It should show like above screen shot. Note that the plugin is nested under observability/, so make sure to use the full path observability/langfuse when enabling it.langfuse at startup, and if the SDK is missing it quietly turns itself into a no-op: no error, no traces, nothing to grep for.pip install langfuse inside the container. It works, until it does not. The official Hermes image only persists /opt/data, so anything you install into the container layer gets wiped the next time Coolify recreates or upgrades the service, and tracing silently dies again. I learned this the annoying way. Make the install durable instead. Two options that survive a redeploy:# Base this on the exact Hermes image tag Coolify deploys
FROM <your-hermes-image>:<tag>
RUN pip install --no-cache-dir langfuse
/opt/data and set PYTHONPATH so the gateway’s Python finds it on every boot, redeploy or not.5. Env vars, and getting them to actually reach the process
The Langfuse plugin reads its configuration from environment variables. Hermes loads these from its own env file at ~/.hermes/.env (see the Hermes environment-variable reference). My mistake early on was dropping the variables into an arbitrary project .env that Hermes never reads. On a Coolify deployment the cleanest option is to set them directly as container environment variables, so they reach the Hermes process no matter where it starts from.HERMES_LANGFUSE_PUBLIC_KEYHERMES_LANGFUSE_SECRET_KEYHERMES_LANGFUSE_BASE_URL You will get these from the Langfuse dashboard after creating a new organization. The public key and secret key are used for authentication, and the base URL is the URL of your self-hosted Langfuse instance.langfuse SDK is not installed (Section 4), the gateway was not restarted after the change, or the base URL is wrong. That is where to look first.A note on the OTLP variables
OTEL_EXPORTER_OTLP_ENDPOINT, like https://your-langfuse-base-url/api/public/otelOTEL_EXPORTER_OTLP_HEADERS, like Authorization=Basic ${AUTH_STRING},x-langfuse-ingestion-version=4 AUTH_STRING with echo -n "pk-lf-...:sk-lf-..." | base64 (add -w 0 on GNU systems, base64 auto-wraps long keys).HERMES_LANGFUSE_* values above. The OTEL_EXPORTER_OTLP_* variables only matter if you are wiring up separate, generic OpenTelemetry instrumentation that ships to Langfuse’s OTLP endpoint. If your three plugin variables are right and the SDK is installed, you do not need them.https://your-langfuse-base-url/api/public/otel). Port 3000 is the internal container port, so http://localhost:3000/api/public/otel is only for a direct local deployment.HERMES_LANGFUSE_ENVHERMES_LANGFUSE_RELEASEHERMES_LANGFUSE_SAMPLE_RATEHERMES_LANGFUSE_DEBUGHERMES_LANGFUSE_MAX_CHARSEnv Var Sample Value Description HERMES_LANGFUSE_PUBLIC_KEY pk-lf-1234567890abcdefThe public key for your Langfuse account. HERMES_LANGFUSE_SECRET_KEY sk-lf-1234567890abcdefThe secret key for your Langfuse account. HERMES_LANGFUSE_BASE_URL https://your-langfuse-instance.comThe base URL of your self-hosted Langfuse instance. OTEL_EXPORTER_OTLP_HEADERS Authorization=Basic <base64(pk:sk)>,x-langfuse-ingestion-version=4Not read by the bundled plugin. Only for a separate generic OpenTelemetry setup. Use Basic auth with the base64 of public_key:secret_key, not a Bearer token.OTEL_EXPORTER_OTLP_ENDPOINT https://your-langfuse-instance.com/api/public/otelNot read by the bundled plugin. Only for a separate generic OpenTelemetry setup. Use your public HTTPS base URL with no port behind a Coolify proxy. http://localhost:3000/api/public/otel is for a direct local deployment.HERMES_LANGFUSE_ENV productionThe environment name for Langfuse. HERMES_LANGFUSE_RELEASE 1.0.0The release version for Langfuse. HERMES_LANGFUSE_SAMPLE_RATE 1.0The sample rate for Langfuse traces. HERMES_LANGFUSE_DEBUG trueEnable debug mode for Langfuse. HERMES_LANGFUSE_MAX_CHARS 1000The maximum number of characters for Langfuse traces. 6. Verifying it works
/proc/$GWPID/environ file. First capture the gateway PID into GWPID, then grep for the variable name only, so you confirm the variable is present without printing its value:GWPID=$(pgrep -f "hermes gateway run" | head -n1)
grep -c HERMES_LANGFUSE_SECRET_KEY /proc/$GWPID/environ
hermes gateway restart in the terminal inside the Coolify container.7. What you actually see
7.1. Dashboards
Langfuse homepage showing Hermes Agent traces
Langfuse Cost Dashboard
Langfuse Usage Dashboard
Langfuse Latency Dashboard7.2. Traces
Langfuse Traces Homepage As you can see from the screenshot all are my cron jobs running at different time intervals. You can click on any of the traces to see the details of the trace. Here is a screenshot of a trace showing the multi-turn session, tool-call spans, and token/cost breakdown across providers:
Detailed Trace showing multi-turn session, tool-call spans, and token/cost breakdown across providers7.3. What the traces actually told me
Cron Jobs Graph
16 Terminal Tool Calls which led to 10 LLM callsminimax-m3 model ran on 13th July and burned through my entire monthly Go plan limit in one go. I still cannot pin down where exactly that model got called from. Getting granular per-model attribution is on my list.
Opencode Usage Graph8. Ops footnotes
HERMES_LANGFUSE_SAMPLE_RATE to 0.5 to reduce the number of traces sent to Langfuse and reduce the cost. I will update this post if I do that and see any difference in the traces. This is one of the reasons my Hetzner disk was filling up fast at ~1.5GB per day.Future Work
Lessons Learned
.env file: The .env file inside the container did not reach the Hermes process. Setting the variables directly in the Coolify environment section was the only thing that worked reliably. When in doubt, put config where the process actually reads it, not where it looks tidy.HERMES_LANGFUSE_* values. An empty dashboard is almost always the missing SDK, a gateway that was not restarted, or a wrong base URL. Look there before you touch anything else.hermes gateway restart (or ask Hermes to do it) or you will keep debugging a process that never picked up your changes.LANGFUSE_S3_EVENT_UPLOAD_* variables was the fix. Budget an hour for storage config, not five minutes.Frequently Asked Questions
Can I add Langfuse to a custom agent that does not use LangChain?
HERMES_LANGFUSE_* variables, no LangChain required. For a different custom agent that has no such plugin, Langfuse also accepts traces over generic OpenTelemetry (OTLP), so any OTel-instrumented app can report to it without framework lock-in.Why are no traces showing up in Langfuse even though the plugin is enabled?
HERMES_LANGFUSE_* values. If the dashboard stays empty, work through this order: confirm the langfuse SDK is installed and survives redeploys, restart the gateway with hermes gateway restart, then check the env vars actually reached the process and the base URL is correct. Adding OTLP exporter variables does not help, because the plugin does not read them.Does Langfuse track cost for non-OpenAI models like DeepSeek or GLM?
deepseek-v4-flash, GLM-5.2 and DeepSeek-V4-Pro through OpenCode, and Langfuse breaks down cost and token usage per model. You may need to check that pricing is configured for less common models, but the provider-agnostic cost view is the main reason I set this up.Do I have to self-host Langfuse or can I use the cloud version?
How much disk does self-hosted Langfuse use?
HERMES_LANGFUSE_SAMPLE_RATE and a retention policy before you run out of disk, not after.Why did the Coolify one-click Langfuse install fail for me?
LANGFUSE_S3_EVENT_UPLOAD_* environment variables with the region set to auto and the R2 endpoint URL. After that the container came up cleanly.How much does this setup cost me?
Related Articles
References