Skip to content

fix(linux): report container memory limits instead of the host's - #1118

Merged
karlitschek merged 1 commit into
masterfrom
fix/container-memory-limits
Sep 9, 2026
Merged

karlitschek merged 1 commit into
masterfrom
fix/container-memory-limits

Conversation

@karlitschek

Copy link
Copy Markdown
Member

Problem

/proc/meminfo is not namespaced. Inside Docker, Kubernetes or Nextcloud AIO the kernel happily reports the host's memory to a container, so Linux::getMemory() has been describing the machine rather than the memory Nextcloud is actually allowed to use.

This surfaces in two places:

  • the Monitoring admin page, where the RAM meter and the memory chart are among the first things an admin looks at
  • the OCS API (mem_total / mem_free / swap_total / swap_free), which is what external monitoring tools graph and alert on

On a 2 GiB container running on a 64 GiB host the reported figure is off by a factor of 32, and — worse than being merely wrong — it is wrong in the reassuring direction: a container about to hit its limit and get OOM-killed shows plenty of free memory right up until it dies.

Change

Where a cgroup memory limit applies, read the limit and usage from the cgroup instead of /proc/meminfo:

  • cgroup v2 (unified hierarchy) is tried first: memory.max, memory.current, memory.stat, memory.swap.max, memory.swap.current
  • cgroup v1 is the fallback: memory.limit_in_bytes, memory.usage_in_bytes, memory.stat, and the memory.memsw.* counters, from which the swap share is derived by subtracting the memory limit

Two details worth calling out:

  • Reclaimable page cache is discounted from the usage (inactive_file on v2, total_inactive_file on v1), so MemAvailable keeps the meaning it has in /proc/meminfo while MemFree stays the stricter number. This is the same accounting docker stats does.
  • A cgroup that is not more restrictive than the host is ignored. That single rule covers both ways the kernel spells "no limit" — the literal max on v2, and the near-PHP_INT_MAX sentinel on v1 — and it also guards against a limit set above the host's physical memory.

Anything unreadable falls back to the previous behaviour, so a host without cgroups, with open_basedir in the way, or in restricted_mode reports exactly what it reported before.

Tests

Six new cases in LinuxTest, plus two memory.stat fixtures:

case expectation
cgroup v2, 2 GiB limit container figures, host's 15947 MB nowhere in the output
cgroup v2, memory.max = max unchanged host figures
cgroup v1, 2 GiB limit + memsw container figures including the derived swap share
cgroup v1, PHP_INT_MAX sentinel unchanged host figures
limit above host memory unchanged host figures
limit but no memory.stat available falls back to free, no cache discount

The existing memory tests needed one adjustment: they pinned readContent to /proc/meminfo with ->with(...), which constrains every invocation. They now go through a small mockFiles() helper that serves a map of paths and throws for anything else — which is exactly what readContent does for a file that does not exist.

Verified locally with PHPUnit 10 (29 tests, 54 assertions in LinuxTest; FreeBSDTest, DummyTest and NetInterfaceTest also still green), and cross-checked by disabling the new code path to confirm the added tests actually fail without it. php-cs-fixer reports no style violations.

Deliberately not in scope

  • CPU. /proc/cpuinfo has the same problem, but the honest fix is not symmetrical: sys_getloadavg() is host-wide, so reporting a container's CPU quota as the core count while continuing to divide the host's load average by it would make the percentage worse, not better (host load 8 on a 16-core host, quota of 2 → "400 %"). Doing this properly means sampling cpu.stat's usage_usec between polls, which is a larger change and belongs in its own PR.
  • Delegated cgroups outside containers. A php-fpm service under systemd with MemoryMax= lives at /sys/fs/cgroup/<slice>/<unit>/, not at the root. Resolving that through /proc/self/cgroup would extend the fix to those setups, but it must not be applied blindly: under cgroup v1 a container's /proc/self/cgroup names a path (/docker/<id>) that is not readable from inside the container, where the root path is. Happy to follow up if you'd like it covered.

@nextcloud-command nextcloud-command added the AI assisted This PR contains AI-assisted commits label Sep 9, 2026
@karlitschek
karlitschek force-pushed the fix/container-memory-limits branch from 9b40e2c to 8563b1c Compare September 9, 2026 13:13
@karlitschek

Copy link
Copy Markdown
Member Author

/proc/meminfo is not namespaced, so inside Docker, Kubernetes or AIO the
Monitoring page and the OCS API reported the host's RAM rather than the
memory Nextcloud is actually allowed to use. On a 2 GiB container running
on a 64 GiB host, that is off by a factor of 32.

Where a cgroup memory limit applies, read the limit and usage from it
instead, preferring the v2 unified hierarchy and falling back to v1.
Reclaimable page cache is discounted from the usage so that MemAvailable
keeps the meaning it has in /proc/meminfo, and swap is taken from
memory.swap.* on v2 and derived from the memsw counters on v1.

A cgroup that is not more restrictive than the host is ignored, which
covers both "max" on v2 and the near-PHP_INT_MAX sentinel on v1, so hosts
without a limit keep reporting exactly what they reported before.

Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com>
Assisted-by: Claude Code:claude-opus-5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@karlitschek
karlitschek force-pushed the fix/container-memory-limits branch from 8563b1c to 7a3df4f Compare September 9, 2026 13:46

@jancborchardt jancborchardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to make sense to use the container limit if it gets reached first

@karlitschek
karlitschek merged commit d1d482f into master Sep 9, 2026
44 checks passed
@karlitschek
karlitschek deleted the fix/container-memory-limits branch September 9, 2026 14:57
@karlitschek

Copy link
Copy Markdown
Member Author

/backport to stable35

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted This PR contains AI-assisted commits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants