Send Node.js logs from Docker to Grafana Cloud with Alloy

tl;dr: alloy config, docker-compose. Any service that's meant to live more than couple of weeks eventually reaches the stage where you feel the need to properly monitor it. You usually start with simple console.log logging, but soon realize it's not readable enough, it's not searchable enough and it's only available on your server. Probably inside Docker container. I was exactly at that point, annoyed by constant need to ssh into my server to check one log line. I've wanted to play with Grafana ecosystem for a while, too. So it seemed to be the perfect moment. In this post, I’ll walk you through a simple and minimal setup that streams my Node.js application's logs into the Grafana Cloud dashboard using Grafana Alloy and Loki. This is what my final setup looks like: Now let’s break down how to make it work. Producing Logs I'm using pino to generate logs from my service. I won't dive deep into the setup as the library outputs raw JSON to stdout by default. An example log line would look like this: {"level":30,"time":1746117737323,"pid":45,"hostname":"96773881a0b3","module":"server.js","env":"development","msg":"Server is running"} Docker’s default logging behavior captures all stdout and stderr from your container and writes them into a file. json-file is the default logging driver in Docker, you can confirm this using: docker info --format '{{.LoggingDriver}}' You can also find out the actual log file location for a specific container: docker inspect -f '{{.LogPath}}' And this exactly what Alloy will be reading and forwarding to Grafana Cloud. Onboarding to Grafana Cloud and Setting Up Loki Before we start wiring things up locally, let's prepare the Grafana Cloud workspace. Navigate to Grafana Cloud and sign up or log in. In the sidebar, select Connections → Add new connection, select Loki. This is the place that prompts you to set up your Loki connection and allows you to generate an access token for Alloy. In section 2, Install Grafana Alloy, click the "Run Grafana Alloy" button to retrieve necessary information. We're interested in the token, Loki username (GCLOUD_HOSTED_LOGS_ID) and Loki URL (GCLOUD_HOSTED_LOGS_URL). You'll need them later to set up Alloy config. Setting Up Alloy Config The configuration for the collector is stored in a file with *.alloy extension. Let's create config.alloy in the project root. // Step 1. Discover Docker containers and extract metadata. discovery.docker "linux" { host = "unix:///var/run/docker.sock" } // Step 2. Extract a service name from the container name using a relabeling rule. discovery.relabel "logs_integrations_docker" { targets = discovery.docker.linux.targets rule { source_labels = ["__meta_docker_container_name"] regex = "/(.*)" target_label = "service_name" } } // Step 3. Collect logs from Docker containers together with relabel information and forward to Loki receiver. loki.source.docker "default" { host = "unix:///var/run/docker.sock" targets = discovery.relabel.logs_integrations_docker.output labels = {"platform" = "docker"} forward_to = [loki.write.cloud.receiver] } // Step 4. Send logs to Grafana Cloud Loki. loki.write "cloud" { endpoint { url = sys.env("GRAFANA_LOKI_URL") basic_auth { username = sys.env("GRAFANA_LOKI_USERNAME") password = sys.env("GRAFANA_CLOUD_API_KEY") } } } This config basically defines a pipeline of operations Alloy performs to collect, transform and deliver the data. In our case, we want to send logs from Docker to Grafana Cloud Loki. To do that, we'll pass the credentials you got earlier to config - using environment variables for safety.

May 2, 2025 - 12:02
 0
Send Node.js logs from Docker to Grafana Cloud with Alloy

tl;dr: alloy config, docker-compose.

Any service that's meant to live more than couple of weeks eventually reaches the stage where you feel the need to properly monitor it. You usually start with simple console.log logging, but soon realize it's not readable enough, it's not searchable enough and it's only available on your server. Probably inside Docker container.

I was exactly at that point, annoyed by constant need to ssh into my server to check one log line. I've wanted to play with Grafana ecosystem for a while, too. So it seemed to be the perfect moment.

In this post, I’ll walk you through a simple and minimal setup that streams my Node.js application's logs into the Grafana Cloud dashboard using Grafana Alloy and Loki.

This is what my final setup looks like:

Final setup with Grafana

Now let’s break down how to make it work.

Producing Logs

I'm using pino to generate logs from my service. I won't dive deep into the setup as the library outputs raw JSON to stdout by default. An example log line would look like this:

{"level":30,"time":1746117737323,"pid":45,"hostname":"96773881a0b3","module":"server.js","env":"development","msg":"Server is running"}

Docker’s default logging behavior captures all stdout and stderr from your container and writes them into a file. json-file is the default logging driver in Docker, you can confirm this using:

docker info --format '{{.LoggingDriver}}'

You can also find out the actual log file location for a specific container:

docker inspect -f '{{.LogPath}}' 

And this exactly what Alloy will be reading and forwarding to Grafana Cloud.

Onboarding to Grafana Cloud and Setting Up Loki

Before we start wiring things up locally, let's prepare the Grafana Cloud workspace.

Navigate to Grafana Cloud and sign up or log in. In the sidebar, select ConnectionsAdd new connection, select Loki. This is the place that prompts you to set up your Loki connection and allows you to generate an access token for Alloy.

In section 2, Install Grafana Alloy, click the "Run Grafana Alloy" button to retrieve necessary information. We're interested in the token, Loki username (GCLOUD_HOSTED_LOGS_ID) and Loki URL (GCLOUD_HOSTED_LOGS_URL). You'll need them later to set up Alloy config.

Image description

Setting Up Alloy Config

The configuration for the collector is stored in a file with *.alloy extension. Let's create config.alloy in the project root.

// Step 1. Discover Docker containers and extract metadata.
discovery.docker "linux" {
  host = "unix:///var/run/docker.sock"
}

// Step 2. Extract a service name from the container name using a relabeling rule.
discovery.relabel "logs_integrations_docker" {
  targets = discovery.docker.linux.targets

  rule {
    source_labels = ["__meta_docker_container_name"]
    regex = "/(.*)"
    target_label = "service_name"
    }
}

// Step 3. Collect logs from Docker containers together with relabel information and forward to Loki receiver.
loki.source.docker "default" {
  host = "unix:///var/run/docker.sock"
  targets = discovery.relabel.logs_integrations_docker.output
  labels = {"platform" = "docker"}
  forward_to = [loki.write.cloud.receiver]
}

// Step 4. Send logs to Grafana Cloud Loki.
loki.write "cloud" {
  endpoint {
    url = sys.env("GRAFANA_LOKI_URL")
    basic_auth {
      username = sys.env("GRAFANA_LOKI_USERNAME")
      password = sys.env("GRAFANA_CLOUD_API_KEY")
    }
  }
}

This config basically defines a pipeline of operations Alloy performs to collect, transform and deliver the data. In our case, we want to send logs from Docker to Grafana Cloud Loki. To do that, we'll pass the credentials you got earlier to config - using environment variables for safety.