Total memory used by all Docker containers: a correct docker stats summary (and why summing MEM % is wrong)
docker stats lists CPU, memory and I/O for every running container, but it never shows a total. To answer "how much memory are my containers using altogether?", you have to add up the column yourself. There's an easy way to get that wrong, and the original version of this post did.
Everything below ran against Docker Engine 29.1.3 (Docker Desktop on macOS, with 7.751 GiB given to its Linux VM). The test containers ran a small Node.js script that allocates and holds a set amount of memory. The shell script was checked with macOS awk, mawk 1.3.4 (debian:13-slim) and BusyBox awk. The Node.js script ran on Node 24.20 and in node:26-slim (Node 26.10).
The test setup
Five containers: two of them have a memory limit (--memory) and three don't. Two of the unlimited ones are a Compose project called shop:
NAME MEM USAGE / LIMIT MEM %
shop-worker-1 61.84MiB / 7.751GiB 0.78%
shop-api-1 162.4MiB / 7.751GiB 2.05%
mem-limited-100 112.3MiB / 128MiB 87.72%
mem-limited-300 318.9MiB / 400MiB 79.73%
mem-unlimited-300 347.6MiB / 7.751GiB 4.38%Adding up the first column gives about 1003 MiB. That's the number we want.
Why summing MEM % is wrong
This is the script from the original post. It sums the MEM % column and multiplies the result by the total memory from docker system info:
mem_amount_total_with_unit=$(docker system info \
| grep 'Total Memory: ' \
| tr -d 'Total Memory: ')
unit=$(echo ${mem_amount_total_with_unit} \
| sed 's/[0-9\.]*//g')
mem_amount_total=$(echo ${mem_amount_total_with_unit} \
| sed 's/[^0-9\.]*//g')
mem_percent_used=$(docker stats --no-stream --format '{{.MemPerc}}' \
| tr -d '%' \
| paste -s -d '+' - \
| bc)
mem_percent_used=${mem_percent_used:-0}
mem_amount_used=$(echo "scale=2; ${mem_amount_total} * ${mem_percent_used} / 100" \
| bc)
echo "Memory Amount Total: ${mem_amount_total}${unit}"
echo "Memory Amount Used: ${mem_amount_used}${unit}"
echo "Memory Percent Used: ${mem_percent_used}%"Memory Amount Total: 7.751GiB
Memory Amount Used: 13.53GiB
Memory Percent Used: 174.66%13.53 GiB used on a 7.75 GiB machine is impossible. The problem is that MEM % is relative to each container's own limit, not to the host. mem-limited-100 shows 87.72% because it uses 112 MiB of its 128 MiB limit, which is only about 1.4% of the host. Percentages with different denominators can't be added. The script only gives the right answer when no container has a limit, so that every percentage happens to be relative to the same total.
Also note what "total" means on Docker Desktop: docker system info reports the memory of Docker's Linux VM (7.751 GiB here), not of your Mac or Windows machine.
The fix: sum MEM USAGE instead
The MemUsage field is an absolute amount (112.3MiB / 128MiB). Take the part before the slash, convert the unit to bytes, and add it up. This version needs only docker and awk, so there's no bc to install:
#!/usr/bin/env bash
# Sum the memory actually used by all running containers.
docker stats --no-stream --format '{{.MemUsage}}' | awk '
function bytes(s, n, u) {
n = s + 0; u = s; sub(/^[0-9.]+/, "", u)
if (u == "KiB") return n * 1024
if (u == "MiB") return n * 1024^2
if (u == "GiB") return n * 1024^3
if (u == "TiB") return n * 1024^4
return n # "B"
}
{ total += bytes($1) }
END { printf "Containers: %d, memory used: %.1f MiB\n", NR, total / 1024^2 }'$ chmod +x docker-mem.sh
$ ./docker-mem.sh
Containers: 5, memory used: 1003.0 MiBmawk and BusyBox awk printed exactly the same line from the same input, so it also works inside Debian- and Alpine-based images. Docker prints binary units (B, KiB, MiB, GiB), which is what the function converts. --no-stream makes docker stats print one snapshot and exit, instead of refreshing forever.
A Node.js version with a per-project breakdown
When several Compose projects run on one machine, a total per project is more useful than a single total. Compose labels every container it creates with com.docker.compose.project, and docker stats can print each row as JSON:
#!/usr/bin/env node
// Memory used by running containers, grouped by Compose project, plus the total.
import { execFileSync } from 'node:child_process';
const UNITS = { B: 1, KiB: 1024, MiB: 1024 ** 2, GiB: 1024 ** 3, TiB: 1024 ** 4 };
const toBytes = (s) => {
const [, n, unit] = s.trim().match(/^([\d.]+)\s*([KMGT]?i?B)$/);
return Number(n) * UNITS[unit];
};
const mib = (b) => `${(b / 1024 ** 2).toFixed(1)} MiB`;
const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8' }).trim().split('\n').filter(Boolean);
// One JSON object per line
const stats = docker('stats', '--no-stream', '--format', '{{json .}}').map((line) => JSON.parse(line));
// Which Compose project (if any) each container belongs to
const projects = Object.fromEntries(
docker('ps', '--format', '{{.ID}}\t{{.Label "com.docker.compose.project"}}').map((l) => l.split('\t')),
);
const groups = new Map();
let total = 0;
for (const s of stats) {
const used = toBytes(s.MemUsage.split('/')[0]);
const group = projects[s.ID.slice(0, 12)] || '(no project)';
groups.set(group, (groups.get(group) ?? 0) + used);
total += used;
}
for (const [name, bytes] of [...groups].sort((a, b) => b[1] - a[1])) console.log(`${name.padEnd(20)} ${mib(bytes).padStart(12)}`);
console.log(`${'TOTAL'.padEnd(20)} ${mib(total).padStart(12)} (${stats.length} containers)`);$ node docker-mem.mjs
(no project) 778.8 MiB
shop 224.2 MiB
TOTAL 1003.0 MiB (5 containers)The total matches the awk script. execFileSync passes the arguments directly to docker without a shell, so the {{json .}} template doesn't need any extra quoting.
If you run the script inside a container, with the Docker socket mounted (-v /var/run/docker.sock:/var/run/docker.sock), remember that the container running it is counted too. In node:26-slim it reported (6 containers) and about 83 MiB more, which was the Node.js container itself.
For a single Compose project, you don't need a script to see its containers: docker compose -p shop stats --no-stream lists only that project's containers. It doesn't add them up either.
What the number actually measures
- Page cache isn't included. According to the Docker docs, on Linux the CLI subtracts cache from the total memory usage (
inactive_fileon cgroup v2 hosts,total_inactive_fileon cgroup v1). A container that has read large files can have a bigger cgroup total thandocker statsshows. That memory can be reclaimed, so this is usually the number you want. - It's container memory, not host memory. The Docker daemon, and on Docker Desktop the VM's own kernel and services, use memory that no container accounts for. Use
free -hon a Linux host, or Docker Desktop's resource usage view, to see the whole picture. - Values are rounded.
docker statsprints values like112.3MiB, so the total can be off by a few hundred kilobytes. That's fine for a summary. - Windows containers report a private working set instead of usage and limit, and have no
MEM %. The scripts above are for Linux containers, which includes Docker Desktop on macOS and Windows.
If you set memory limits on your containers (and you should, especially for Node.js, see Running Node.js with Docker), the summed MEM % approach is guaranteed to be wrong. Sum MEM USAGE.
Sources & further reading
- Docker docs: docker container stats (
--no-stream,--format, and how memory usage is calculated on Linux) - Docker docs: resource constraints (
--memory) - Docker docs: docker compose stats
- Docker docs: Compose project names
- Linux kernel docs: cgroup v2 memory interface files (
memory.current,memory.stat,inactive_file) - Node.js docs: child_process.execFileSync()