If you've been monitoring Linux servers for any length of time, there's a good chance node_exporter was the first thing you installed. It's lightweight, reliable, and exposes a huge amount of machine metrics for Prometheus to scrape. For years, it has been the default answer.
As your infrastructure grows, though, your monitoring stack usually grows with it. First comes log collection. Then traces. Before long you're running node_exporter
, a log shipper, and maybe another telemetry agent. Each component has its own configuration, service unit, upgrade cycle, and failure modes.
Grafana Alloy changes that by consolidating those responsibilities into a single telemetry agent.
This post walks through migrating from node_exporter
to Alloy on a real fleet, one server at a time, while maintaining continuous visibility throughout the process. These are the exact steps that survived contact with production on the Irin monitoring stack, not the idealized version that looks clean in a diagram.
TL;DR If you're already running node_exporter
, don't replace it overnight. Install Grafana Alloy alongside it, configure Alloy's built-in prometheus.exporter.unix component, verify that metrics are reaching your remote Prometheus instance, and only then retire node_exporter.
Migrating one server at a time minimizes risk, preserves visibility, and positions your infrastructure for logs, traces, and future telemetry without deploying additional agents.
Before getting started, it's worth understanding what actually changes.
This isn't simply replacing one monitoring agent with another.
node_exporter
is a server. It listens on a port, typically 9100,and waits for Prometheus to connect and scrape metrics. That means every monitored machine needs an open endpoint, network connectivity from Prometheus, firewall rules, and scrape configurations.
Alloy flips that model around.
Instead of waiting for Prometheus to connect, Alloy collects metrics locally and pushes them to a remote endpoint using Prometheus Remote Write.
On my stack, that outbound traffic travels through a Cloudflare Tunnel. Nothing reaches into the monitored servers. There are no metrics ports exposed to the LAN, no inbound firewall rules to maintain, and no scrape network that has to remain routable. The user’s metrics are exposed through the secure tunnel, and the monitoring stack has no access to the server.
That shift is the real migration, you're not replacing a binary, you're changing the direction your telemetry flows. Once you frame it that way, the rest of the migration makes much more sense.
Alloy is configured using River, which is less like a traditional configuration file and more like a telemetry pipeline. Each component performs one task before handing data to the next component. As you begin to put your model together, the configuration becomes surprisingly readable.
The component that replaces node_exporter is prometheus.exporter.unix
.
Under the hood it's using the same collector code as node_exporter
, so the metrics themselves remain familiar.
A minimal configuration looks like this:
// Collect host metrics.
prometheus.exporter.unix "host" {
}
// Scrape those locally collected metrics.
prometheus.scrape "host" {
targets = prometheus.exporter.unix.host.targets
forward_to = [prometheus.remote_write.default.receiver]
}
// Push metrics to Prometheus Remote Write.
prometheus.remote_write "default" {
endpoint {
url = "https://metrics.example.internal/api/v1/write"
}
}
Read from top to bottom, it tells a story:
Instead of maintaining a YAML file full of scrape targets, you're wiring components together into a pipeline. That same pattern extends naturally to logs, traces, profiling, and nearly every other telemetry signal Alloy supports.
One thing to remember during migration: the metrics themselves stay the same, but some labels—particularly the instance
label—may change depending on how Alloy identifies the host.
That's expected, and it's important when you start verifying your migration.
The temptation is to deploy Alloy everywhere and immediately disable node_exporter, but the best way is to do it slowly. The safest migration pattern is a canary. Pick a non-critical server, install Alloy beside node_exporter, and let both run simultaneously while you verify that the new telemetry path is working correctly. The resource overhead is negligible, but the confidence you gain is enormous.
Running both agents briefly means you always have a known-good monitoring path while validating the new one. Only after you've confirmed that Alloy is producing fresh, accurate metrics should you retire node_exporter
. Once you've done that successfully a few times, batching servers becomes much less stressful.
Choose a stable server where a few minutes of metric oddities wouldn't be catastrophic, but would still be noticeable.
Install Grafana Alloy using your distribution's package manager.
Most Linux distributions install Alloy as a systemd service automatically, with the primary configuration file located at:
/etc/alloy/config.alloy
Enable the service, but leave node_exporter exactly as it is.
sudo systemctl enable alloy
sudo systemctl start alloy
At this point nothing has changed from Prometheus' perspective, you’re just running two collector concurrently.
Create your River configuration and point the remote_write
endpoint at your Prometheus receiver.
Start Alloy and verify the service is healthy.
sudo systemctl status alloy --no-pager
The --no-pager
option simply prints the status and returns you to the shell instead of opening the interactive pager, making it much friendlier for automation and scripting. Alloy also exposes a built-in UI on port 12345. Opening it gives you a live view of every component in your telemetry pipeline.
If prometheus.exporter.unix
and prometheus.remote_write
are healthy, your data is flowing.
If remote_write
is unhealthy, the problem is usually one of three things:
• Incorrect endpoint URL
• Network connectivity
• Authentication
...in roughly that order.
Now both node_exporter
and Alloy are reporting metrics.
First, verify Alloy locally.
curl http://localhost:12345/metrics | head
or check for a specific metric:
curl http://localhost:12345/metrics | grep node_memory_MemAvailable_bytes
Then open Prometheus or Grafana and confirm the new series is arriving. Since Alloy pushes via remote_write instead of being scraped, the standard up metric won't reflect this host anymore — up is generated per scrape target, and there's no scrape target here. Checking it after cutover will look like the host vanished, even though everything is working.
Instead, check for freshness directly:
time() - timestamp(node_uname_info{instance="canary-host"})
A small, stable number means data is arriving on schedule. A growing number means the host has stopped pushing.
Then compare a real metric such as available memory:
node_memory_MemAvailable_bytes
Don't expect the labels to match perfectly — the important part is that the values agree and the new series continues updating.
If your dashboards, recording rules, or alerting rules explicitly reference labels such as job="node_exporter" — or reference up for these hosts — now is a good time to identify them before removing the old exporter.
Once you're confident Alloy is working correctly, stop the old exporter.
sudo systemctl stop node_exporter
sudo systemctl disable node_exporter
stop
ends the running process.
disable prevents it from quietly returning after the next reboot.
Then remove the server's scrape job from Prometheus and reload the configuration.
curl -X POST http://localhost:9090/-/reload
Re avoids interrupting metric collection for every other host while updating the configuration.
Give the server at least one scrape interval under the new path.
Verify that:
• Metrics remain fresh.
• Alerts continue behaving normally.
• Dashboards still populate.
• No recording rules broke because of label changes.
Once everything looks healthy, repeat the process on the next server.
After a handful of successful migrations, you'll have enough confidence to migrate small batches instead of individual hosts.
There are a few issues that show up repeatedly during migrations:
• Removing node_exporter before verifying Alloy.
• Forgetting to reload Prometheus after removing scrape targets.
• Alert rules or dashboards still referencing the old job label.
• Firewall rules blocking outbound Remote Write traffic.
• Assuming label changes won't affect existing dashboards.
None of these are difficult to fix, but catching them during a canary migration is far less stressful than discovering them after migrating twenty servers.
The obvious benefit is consolidation.
Instead of deploying separate agents for metrics, logs, and traces, Alloy provides a single telemetry pipeline that grows with your infrastructure.
If you need logs, you can add a Loki component, for OTLP traces, add another component. The overall architecture doesn't change.
The less obvious benefit is security. Once every monitored machine pushes telemetry outward, you can close your metrics ports entirely. There's no longer a listening endpoint on every server, no scrape network to maintain, and no inbound firewall rule whose only purpose is monitoring.
For anyone managing customer infrastructure—or simply trying to reduce attack surface—that's arguably the biggest improvement Alloy brings.
Five years ago, exposing port 9100 across an internal network wasn't unusual, but today we're steadily moving toward zero-trust networking, outbound-only connectivity, and centralized telemetry pipelines.
Grafana Alloy isn't compelling because it replaces node_exporter. It's compelling because it aligns your monitoring architecture with where modern infrastructure is already heading, and strengthens your security posture by removing exposed enpoints.
Done this way, there should never be a moment when a server isn't being watched.