How to Monitor Docker Containers with New Relic

docker stats is great until you close the terminal. Then it’s gone: no history, no alerts, no way to answer “was this container struggling at 3 AM?” For that you want the numbers shipped somewhere that keeps them, and New Relic is one of the easiest places to send them. Its free tier is generous enough to monitor a few Docker hosts without paying anything.

This guide sets it up for real: a fresh free New Relic account, the infrastructure agent on a RHEL 9 Docker host, three containers, and an alert that actually fires. Every output below comes from that setup, including the two mistakes that cost us time, so they won’t cost you any.

How New Relic monitors Docker

There’s no separate “Docker agent” to install. The New Relic infrastructure agent ships with a Docker integration (nri-docker) built in. When the agent starts and finds /var/run/docker.sock, it reads every container’s stats from Docker and cgroups and sends them every 15 seconds as ContainerSample events. You get CPU, memory, disk I/O, network, restart count, status, image, and every Docker label.

You can see the built-in config right after install:

cat /etc/newrelic-infra/integrations.d/docker-config.yml
integrations:
  - name: nri-docker
    when:
      feature: docker_enabled
      file_exists: /var/run/docker.sock
    interval: 15s
  - name: nri-docker
    when:
      feature: docker_enabled
      env_exists:
        FARGATE: "true"
    interval: 15s

So the whole job is: install the agent on the Docker host, give it a license key, done. The rest of this post is about getting the key right and reading the data properly, which is where people actually get stuck.

What you need

  • A Linux host running Docker. We used RHEL 9.6 with Docker 29.8.2.
  • A New Relic account. The free tier gives you 100 GB of data a month and one full user, no credit card needed. Sign up at newrelic.com/signup.
  • Your account’s license key. Read the next section before you copy it.

Getting the right key (the Copy key ID trap)

In New Relic, click your name at the bottom left, then API keys. You’ll see a row of type INGEST – LICENSE. That’s the one the agent needs.

Click the … menu on that row and you get two options next to each other: Copy key and Copy key ID. Pick the wrong one and you get a 64-character hex string that looks very much like a key, but isn’t. It’s New Relic’s internal ID for the key. We did exactly that, and the agent installed and started happily, then got this every few seconds:

level=warning msg="Commands initial fetch failed." component=AgentService error="unsuccessful response, status:401 []"
level=warning msg="agent connect attempt failed" component=IdentityConnectService error="ingest rejected connect: 401 401 Unauthorized "

A real license key is 40 characters and newer ones end in NRAL. If yours is 64 characters, you copied the key ID. Go back and use Copy key.

Note: the other key types (NRAK-... user keys, NRJS-... browser keys) won’t work as a license key either. A user key is still handy though: it’s what lets you query your data from scripts, which we’ll use later.

Step 1: Run some containers worth watching

Monitoring idle containers teaches you nothing, so our test setup has one container that’s deliberately squeezed. Three services in a Compose file, with labels so we can group them in New Relic later:

services:
  web:
    image: nginx:1.29-alpine
    container_name: demo-web
    ports:
      - "8080:80"
    labels:
      app: shop
      tier: frontend
    restart: unless-stopped
  cache:
    image: redis:8-alpine
    container_name: demo-cache
    labels:
      app: shop
      tier: cache
    restart: unless-stopped
  worker:
    image: alpine:3.22
    container_name: demo-worker
    command: sh -c "while true; do sha256sum /dev/zero | head -c 300000000 >/dev/null; sleep 2; done"
    mem_limit: 128m
    cpus: 0.5
    labels:
      app: shop
      tier: worker
    restart: unless-stopped
docker compose up -d
docker ps
NAMES         IMAGE               STATUS                  PORTS
demo-web      nginx:1.29-alpine   Up Less than a second   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
demo-cache    redis:8-alpine      Up 1 second             6379/tcp
demo-worker   alpine:3.22         Up 1 second

docker stats --no-stream
NAME          CPU %     MEM USAGE / LIMIT
demo-web      0.00%     3.23MiB / 3.379GiB
demo-cache    0.33%     9.816MiB / 3.379GiB
demo-worker   49.53%    644KiB / 128MiB

The worker wants a whole CPU core but is capped at half of one. Keep an eye on that 49.53%. It’s going to be misleading in a moment.

Step 2: Install the New Relic infrastructure agent

New Relic’s web UI has a guided install (Integrations & Agents, then Linux) that generates a one-line command for you. Doing it manually is just as quick and shows what’s actually happening. On RHEL 9, Rocky or Alma:

echo "license_key: YOUR_LICENSE_KEY" | sudo tee -a /etc/newrelic-infra.yml

sudo curl -o /etc/yum.repos.d/newrelic-infra.repo https://download.newrelic.com/infrastructure_agent/linux/yum/el/9/x86_64/newrelic-infra.repo
sudo dnf -q makecache -y --disablerepo="*" --enablerepo="newrelic-infra"
sudo dnf install -y newrelic-infra
 newrelic-infra      x86_64      1.80.5-1.el9         newrelic-infra       57 M
Complete!

newrelic-infra --version
New Relic Infrastructure Agent version: 1.80.5, GoVersion: go1.26.8, GitCommit: 2022083bacc239b5748feee44265b0e341a9de91, BuildDate: 2026-09-24T09:42:35Z

Write the key file before installing the package. The package starts the service right away, and it’ll start without a key otherwise. On Ubuntu and Debian the steps are the same idea with New Relic’s apt repo; the guided install in the UI generates the right commands for your distro.

Lock the key file down, since it holds a credential, then restart and check the log:

sudo chmod 640 /etc/newrelic-infra.yml
sudo systemctl restart newrelic-infra

sudo journalctl -u newrelic-infra --no-pager -o cat | grep -E "connectivity|docker|connect got id"
level=info msg="Checking network connectivity..." component=AgentService service=newrelic-infra
level=info msg="docker sampler enabled" component=ContainerSampler
level=info msg="connect got id" agent-guid=ODU5NDgyN3xJTkZSQXxOQXw0NzUxMjAxNjQzMDg2MTMzNDEz agent-id=4751201643086133413 component=IdentityConnectService

Two lines to look for. docker sampler enabled means it found Docker. connect got id means New Relic accepted the key and registered the host. If you see 401 instead, go back to the key section.

You’ll probably also see this one, and it looks scarier than it is:

level=error msg="cannot build supervisor executor" component=integrations.Supervisor error="failed to load log configs" process=log-forwarder

It just means log forwarding has nothing configured. /etc/newrelic-infra/logging.d/ only contains .example files out of the box. Metrics aren’t affected. If you want container logs in New Relic too, copy one of those examples to a .yml file and point it at your logs.

Step 3: Find your containers in New Relic

Give it a minute or two. In the New Relic UI, open Infrastructure and you’ll find the host, with a containers view listing each running container and its CPU, memory and status.

Here’s our Docker host in that view. CPU sits around 50% because of the throttled worker container, and Processes running shows its sha256sum process at 97%. The agent sees container processes on the host like any other process, which is handy when you need to tie a busy process back to a container:

New Relic Infrastructure host overview for the Docker host app1: CPU near 50%, network traffic, disk usage and the demo-worker container process sha256sum at 97% CPU

The fastest way to confirm data is arriving, though, is a query. Click Query your data (bottom left) and run NRQL, New Relic’s SQL-like query language:

SELECT count(*) FROM SystemSample, ContainerSample SINCE 10 minutes ago FACET eventType()
SystemSample      26
ContainerSample   15

SystemSample is the host. ContainerSample is your containers. If the second one is missing, the agent can’t see Docker; check that /var/run/docker.sock exists and that the log says docker sampler enabled.

Your Docker labels come through automatically as label.* attributes, so you can slice by them. All three of our containers carry app: shop:

SELECT uniqueCount(name) FROM ContainerSample WHERE label.app = 'shop' SINCE 5 minutes ago
uniqueCount.name   3

This is a good reason to label containers properly in Compose or docker run. Labels become your filters and facets in New Relic for free.

Step 4: Read container CPU correctly

Here’s the most useful thing in this whole post. Let’s pull CPU, limits and memory for every container:

SELECT latest(cpuPercent), latest(cpuUsedCoresPercent), latest(cpuLimitCores),
       latest(memoryUsageBytes)/1048576 AS memMiB, latest(memorySizeLimitBytes)/1048576 AS limitMiB,
       latest(memoryUsageLimitPercent), latest(restartCount)
FROM ContainerSample FACET name SINCE 5 minutes ago
name          cpuPercent  cpuUsedCoresPercent  cpuLimitCores  memMiB  limitMiB  memoryUsageLimitPercent  restartCount
demo-worker   50.14       99.60                0.5            0.22    128       0.17                     0
demo-cache    0.29        0.14                 2.0            9.25    null      null                     0
demo-web      0.00        0.00                 2.0            2.59    null      null                     0

Look at the worker. cpuPercent says 50%. That’s the same number docker stats showed, and on its own it reads as “half busy, plenty of room.” Wrong. cpuUsedCoresPercent says 99.6%, because it measures usage against the container’s own limit of 0.5 cores. The worker is completely pinned.

  • cpuPercent: CPU used as a percentage of one host core. Same idea as docker stats.
  • cpuUsedCoresPercent: CPU used as a percentage of the container’s CPU limit. When there’s no limit, the limit is all host cores (2.0 here).

For containers with CPU limits, cpuUsedCoresPercent is the one to watch and alert on. Throttling confirms the worker is being held back, but read this metric carefully, because the name is misleading. cpuThrottleTimeMs is a running total since the container started, not a per-sample value, and its number lines up with the kernel’s throttled_usec in seconds. Here’s the kernel counter on the host and what New Relic reported at the same moment:

cat /sys/fs/cgroup/system.slice/docker-$(docker inspect -f {{.Id}} demo-worker).scope/cpu.stat | grep throttled
nr_throttled 30749
throttled_usec 1494136804

# New Relic, same container
cpuThrottlePeriods   30749
cpuThrottleTimeMs    1494.136804

Same counters: 30,749 throttled periods, 1,494 seconds of throttling in total. So averaging it tells you nothing useful. What you want is how much it grew over a window:

SELECT max(cpuThrottleTimeMs) - min(cpuThrottleTimeMs) AS 'throttled seconds' FROM ContainerSample FACET name SINCE '2026-10-07 15:51:00 +0000' UNTIL '2026-10-07 15:56:00 +0000'
demo-worker   throttled seconds 137.39
demo-cache    throttled seconds 0
demo-web      throttled seconds 0

137 seconds of throttling in a 5-minute window, close to half the time. derivative(cpuThrottleTimeMs, 1 minute) gives the same thing as a rate (28.9 seconds per minute here) and works nicely in charts. That’s the CFS scheduler pausing the container whenever it uses up its quota, and it’s the classic hidden cause of “my app is slow but CPU only shows 50%.”

Memory works the same way. memoryUsageLimitPercent is only set when a container has a memory limit; for unlimited containers it’s null. One more reason to set limits: without them, New Relic can’t tell you how close a container is to running out.

Here’s the CPU-against-limit picture as a chart from the same account (chart times are UTC). The worker sits near 100% of its limit, then drops to about 64% after the fix in the next step:

New Relic chart of cpuUsedCoresPercent per container: demo-worker near 100% of its CPU limit, demo-web and demo-cache near 0

Step 5: Alert when a container hits its limit

Charts are for looking at. Alerts are for when you aren’t. In New Relic, go to Alerts, create an alert policy, then add a condition and choose Write your own query. Our condition:

  • Query: SELECT max(cpuUsedCoresPercent) FROM ContainerSample FACET name
  • Threshold: critical when above 90 for at least 2 minutes
  • Window: 1 minute, event flow, 2-minute delay (the defaults are fine)

FACET name is the important part. It makes one signal per container, so each one alerts on its own and the incident tells you exactly which container broke. A few minutes later:

SELECT timestamp, event, conditionName, priority, title FROM NrAiIncident SINCE 60 minutes ago
event: open
priority: critical
conditionName: Container CPU at its limit
entity.name: demo-worker
title: demo-worker query result is > 90.0 for 2 minutes on 'Container CPU at its limit'

Only demo-worker fired. The web and cache containers stayed quiet, which is exactly what you want from an alert. To get a notification and not just an incident, attach a destination (email, Slack, PagerDuty) to the policy under Alerts > Workflows.

Then we fixed the cause, giving the worker more CPU without restarting it:

docker update --cpus 1.5 demo-worker
demo-worker
docker inspect demo-worker --format '{{.HostConfig.NanoCpus}}'
1500000000

Within a couple of minutes the worker’s numbers flipped, and it’s worth seeing why:

SELECT latest(cpuUsedCoresPercent), latest(cpuLimitCores), latest(cpuPercent) FROM ContainerSample WHERE name = 'demo-worker' SINCE 2 minutes ago
cpuPercent           97.69
cpuUsedCoresPercent  64.52
cpuLimitCores        1.5

cpuPercent went up, from 50% to almost 98%, because the worker now gets a whole core. Watch only that number and you’d think things got worse. cpuUsedCoresPercent went down to 64.5% of its new 1.5-core limit: more work done, no longer pinned. That’s the fix working. New Relic closed the incident by itself:

event: open    2026-10-07 20:57:53   demo-worker   critical
event: close   2026-10-07 21:07:55   demo-worker   critical   closeCause: EVALUATOR

Ten minutes from open to close, with nobody touching New Relic.

Running the agent as a container instead

If you’d rather not install packages on the host (on a container-optimized OS, for example), New Relic publishes the agent as the newrelic/infrastructure image. It needs to see the host and the Docker socket, so it runs privileged with the host’s root filesystem mounted read-only. New Relic’s docs give this command:

docker run -d --name newrelic-infra --network=host --cap-add=SYS_PTRACE --privileged --pid=host 
  -v "/:/host:ro" -v "/var/run/docker.sock:/var/run/docker.sock" 
  -e NRIA_LICENSE_KEY=YOUR_LICENSE_KEY 
  newrelic/infrastructure:latest

We used the package install for this post. Pick one or the other, not both, or you’ll report the same host twice.

Setup confirmation

Agent running and enabled on the Docker host:

systemctl status newrelic-infra --no-pager
● newrelic-infra.service - New Relic Infrastructure Agent
     Loaded: loaded (/etc/systemd/system/newrelic-infra.service; enabled; preset: disabled)
     Active: active (running) since Wed 2026-10-07 20:49:00 PKT; 25s ago
   Main PID: 4355 (newrelic-infra-)
      Tasks: 17 (limit: 21727)
     Memory: 28.1M (max: 1.0G available: 995.8M)

sudo cat /etc/newrelic-infra.yml
license_key: b444bc44********************************

28 MB of RAM for the agent. Light enough to put on anything.

New Relic receiving container data, all three containers reporting with their labels and limits (the NRQL results in steps 3 and 4 come from this same account). Here’s the whole run as a container dashboard, from the throttled worker to the fix at 16:00 UTC. Read the top row together: cpuUsedCoresPercent drops from about 99% to 64% while cpuPercent jumps from 50% to 98%, throttling falls from about 29 seconds per minute to zero, and the CPU limit panel shows exactly when docker update landed:

New Relic Docker container dashboard: CPU percent of limit, cpuPercent, CPU throttling per minute, memory, CPU limit and container status, before and after raising the demo-worker CPU limit

And the side-by-side view that makes the CPU point obvious, cpuPercent against cpuUsedCoresPercent for each container:

New Relic table: demo-worker cpuPercent about 50 but cpuUsedCoresPercent about 99, demo-cache and demo-web near 0

Plus the alert lifecycle shown in step 5: an incident opening for the one container that hit its limit, then closing on its own ten minutes later once the limit was raised.

Frequently asked questions

Is New Relic Docker monitoring free?

The free tier covers it: 100 GB of ingested data per month and one full platform user, with no time limit. One Docker host is a small fraction of that. In our test, the host plus three containers produced about 1.27 MB of infrastructure data in a 25-minute window, which works out to roughly 2.2 GB a month. Plenty of room before you’d pay anything.

Do I need to install anything inside my containers?

No. The agent runs on the host and reads container stats from Docker and the kernel’s cgroups. Your images stay untouched. You’d only add something inside a container for APM, meaning code-level tracing with a language agent (Node, Python, Java and so on).

Why does New Relic show different CPU numbers than docker stats?

It mostly doesn’t. cpuPercent matches docker stats (we saw 50.14% vs 49.53%; they’re sampled at different moments). The difference is that New Relic also gives you cpuUsedCoresPercent, measured against the container’s CPU limit, which docker stats doesn’t show at all.

My containers don’t show up. What should I check?

In order: the agent log has connect got id (key accepted), the log has docker sampler enabled (Docker found), /var/run/docker.sock exists, and you’re looking at the right account. If you run Docker rootless or with a non-standard socket path, the default integration config won’t find it.

Can I monitor Kubernetes the same way?

Not with this setup. Kubernetes has its own New Relic integration, installed with Helm, that understands pods, deployments and nodes. This guide is for plain Docker and Docker Compose hosts. For a broader look at host-level tools, see Linux system monitoring, and if you’re comparing self-hosted options, our Zabbix install guide covers the open-source route.

Asif Khan

Asif Khan

DevOps, Linux, and Cloud engineer with 10+ years building infrastructure that stays up, and automation so I do not have to fix it at 2am. My focus is automation, security, and systems built to survive failure, using Infrastructure as Code, CI/CD, containers, monitoring, and cloud platforms instead of manual fixes. This blog is where I write down what worked, and just as often what didn't the first time around. Real tutorials you can run yourself, troubleshooting notes pulled from actual outages, and the kind of detail you only get once you've hit the problem firsthand. If you're trying to get Linux, DevOps, and cloud infrastructure to behave, that's what you'll find here: reproducible solutions you can understand, test, and apply in your own environment.