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:

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

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:

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

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.