You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
🐞 Bug: System memory usage is wrong when Arcane runs in Docker inside an LXC container #3741
When Arcane runs in Docker inside an unprivileged LXC container, the System Overview memory figures do not describe the LXC. applyCgroupLimits in backend/api/ws/system_stats.go is a deliberate no-op inside Docker:
// It is intentionally a no-op inside Docker: Docker's --cpus / --memory flags// set artificial cgroup constraints that are unrelated to the host totals we// want to display. gopsutil already reads the correct host values there (via// the bind-mounted /proc).ifcgroup.IsDockerContainer() {
returncpuCount, memUsed, memTotal
}
That reasoning is right for Docker on bare metal, where a container's /proc/meminfo really is the machine's. It does not hold when the Docker host is itself an LXC container, which is how the community-scripts Docker LXC and most Proxmox homelabs deploy Arcane.
This is distinct from #913 / #3161 / #2416, which covered Arcane running directly in an LXC and were fixed by the non-Docker branch of this same function. The Docker-inside-LXC path still falls through to gopsutil.
There are two failure modes depending on whether lxcfs files are bound into the container:
Default (no lxcfs binds) — gopsutil reads the physical hypervisor's/proc/meminfo. On a 32 GiB LXC on a 62 GiB host, Arcane reports 62 GiB total and host-wide usage.
With lxcfs binds — lxcfs answers /proc/meminfo against the cgroup of the reading process, so Arcane sees only its own footprint and "system memory used" collapses to near zero. This is becoming more common: it is the standard fix for nested Docker resource visibility, and I have a PR adding it to the community-scripts engine (setup_docker: make LXC resource limits visible to nested containers community-scripts/core#9), which would make it the default for every Docker LXC created by those scripts.
Note the Docker info panel is correct in both cases — dockerClient.Info() is answered by the daemon, which is a plain LXC process and therefore reads the LXC's own values. Only the live system-stats websocket is wrong.
CPU is unaffected: /proc/stat is not cgroup-scoped, so CPU percentages remain right.
Steps To Reproduce
Create an unprivileged Proxmox LXC with features: nesting=1 and a memory limit below the host's (e.g. 32768 MB on a 62 GiB host).
Install Docker inside it and deploy Arcane with the recommended compose.
Open Dashboard → System Overview and compare the memory figures with free -m run inside the LXC.
Expected Behavior
The LXC's total and the LXC's whole memory usage — what free reports inside the LXC, and what Arcane would show if the same LXC were a VM. Usage should include everything in the container: the Docker daemon, sibling containers, and non-Docker processes.
Actual Behavior
Either the hypervisor's totals and usage (case 1), or Arcane's own footprint presented as system usage (case 2). Measured on a 32 GiB LXC with ~109 MiB actually in use, case 2 reports:
Total is already available and correct — keep using info.MemTotal from dockerClient.Info().
Usage can come from the LXC's own cgroup, and no new bind mount is required, because docker/examples/compose.basic.yaml already ships cgroup: host. With cgroupns=host, /sys/fs/cgroup inside the container is the LXC's cgroup root:
$ docker run --rm --cgroupns=host alpine sh -c 'cat /proc/self/cgroup; cat /sys/fs/cgroup/memory.current'
0::/system.slice/docker-febf150a280b154dd408f619a3fe804e41c92a68bde3ead1f8cb6af8f824c7bc.scope
307421184 # LXC-wide (its own cgroup reads 1536000)
LXC truth: /sys/fs/cgroup/memory.current = 284708864
The derivation is the part worth getting right.memory.current includes page cache and overstates badly against /proc/meminfo semantics. Same 32 GiB LXC, all four numbers taken together:
So memory.current - file (from memory.stat) is the one that matches what free shows inside the LXC.
One trap: do not try to read the total from that cgroup./sys/fs/cgroup/memory.max at the LXC root reads max — the real limit lives host-side at /sys/fs/cgroup/lxc/<vmid>/memory.max and is invisible from inside the guest. The Docker API value is the correct source.
For detecting "Docker inside LXC" versus "Docker on bare metal", one signal you already have: compare info.MemTotal from the Docker API against the machine total gopsutil reports. On bare metal they agree; in an LXC the daemon reports the LXC's smaller limit while gopsutil reports the hypervisor's. I have not implemented or verified that heuristic, so treat it as a suggestion rather than a recommendation.
Alternatives Considered
As a stopgap I publish the LXC's own /proc/meminfo to a plain file from a small systemd unit inside the LXC (the read is taken by an LXC-level process, so lxcfs scopes it correctly) and bind it over Arcane's /proc/meminfo. That works today and gives exactly the target numbers:
But it needs a refresher daemon, a compose bind, and carries up to one refresh interval of staleness. Reading the cgroup directly needs none of that, which is why I would rather see it in Arcane.
Arcane Version
v2.9.0
Installation Method
Docker Compose (Recommended)
Environment Type
Local Docker environment
Database Type
SQLite (default)
Operating System
Debian 13 (unprivileged LXC on Proxmox VE 9.2.10, lxcfs 7.0.0-pve1, Docker 29.7.2)
Additional Context
All figures above were measured on live hardware: unprivileged LXC, features: nesting=1, 32768 MB / 6 cores on a 62 GiB host, plus a second 2048 MB LXC used for the stopgap verification.
Reproduced on a completely stock deployment rather than my own setup, in case that is useful for triage.
Built with the community-scripts Docker LXC (ct/docker.sh, default install, 2 GiB / 2 cores, unprivileged, features: nesting=1,keyctl=1) and then Arcane via their tools/addon/arcane.sh, which fetches your docker/examples/compose.basic.yaml and runs docker compose up -d. Nothing modified.
LXC truth (2 GiB LXC on a 62 GiB host):
MemTotal: 2097152 kB MemFree: 1634840 kB Cached: 235836 kB nproc=2
Arcane container sees:
MemTotal: 65648168 kB MemFree: 46440020 kB Cached: 6111572 kB
Docker API (daemon):
MemTotal=2147483648 NCPU=2
The container sees the entire hypervisor — 31x the LXC's real memory — and host-wide usage that includes every other guest on the machine. The Docker info panel in the same UI reports 2 GiB correctly, so a single dashboard shows two contradictory totals.
Two things this confirms about the proposal in the issue body:
cgroupns=host is already in effect on a stock install — docker inspect -f '{{.HostConfig.CgroupnsMode}}' arcane returns host, courtesy of the cgroup: host line in your own compose.basic.yaml. No new bind mount is required to reach the LXC's cgroup.
The memory.current - file formula holds here too. Read from inside the stock container:
memory.current = 484130816
memory.max = max <- confirms the total cannot come from here
file = 242016256
target (MemTotal - MemAvailable) = 226 MiB
memory.current = 461 MiB (2x too high)
memory.current - file = 231 MiB (within 5 MiB)
Worth noting for anyone who finds this issue: installing Arcane natively in the LXC instead of in Docker sidesteps the problem entirely, because an ordinary LXC process reads the correct values straight from /proc/meminfo — that is the !cgroup.IsDockerContainer() path added for #913. The gap is specifically Docker-inside-LXC.
Ive spent alot of time trying to figure out lxc cgroups, i thougth i fixed it, but isnt it considered bad practice to run docker inside of an LXC? Ill try to to look again, but im not sure i want to spend abother 8 hours trying to figure this out when its generally frowned upon to run it that way.
Bug Description
When Arcane runs in Docker inside an unprivileged LXC container, the System Overview memory figures do not describe the LXC.
applyCgroupLimitsinbackend/api/ws/system_stats.gois a deliberate no-op inside Docker:That reasoning is right for Docker on bare metal, where a container's
/proc/meminforeally is the machine's. It does not hold when the Docker host is itself an LXC container, which is how the community-scripts Docker LXC and most Proxmox homelabs deploy Arcane.This is distinct from #913 / #3161 / #2416, which covered Arcane running directly in an LXC and were fixed by the non-Docker branch of this same function. The Docker-inside-LXC path still falls through to gopsutil.
There are two failure modes depending on whether lxcfs files are bound into the container:
/proc/meminfo. On a 32 GiB LXC on a 62 GiB host, Arcane reports 62 GiB total and host-wide usage./proc/meminfoagainst the cgroup of the reading process, so Arcane sees only its own footprint and "system memory used" collapses to near zero. This is becoming more common: it is the standard fix for nested Docker resource visibility, and I have a PR adding it to the community-scripts engine (setup_docker: make LXC resource limits visible to nested containers community-scripts/core#9), which would make it the default for every Docker LXC created by those scripts.Note the Docker info panel is correct in both cases —
dockerClient.Info()is answered by the daemon, which is a plain LXC process and therefore reads the LXC's own values. Only the live system-stats websocket is wrong.CPU is unaffected:
/proc/statis not cgroup-scoped, so CPU percentages remain right.Steps To Reproduce
features: nesting=1and a memory limit below the host's (e.g. 32768 MB on a 62 GiB host).free -mrun inside the LXC.Expected Behavior
The LXC's total and the LXC's whole memory usage — what
freereports inside the LXC, and what Arcane would show if the same LXC were a VM. Usage should include everything in the container: the Docker daemon, sibling containers, and non-Docker processes.Actual Behavior
Either the hypervisor's totals and usage (case 1), or Arcane's own footprint presented as system usage (case 2). Measured on a 32 GiB LXC with ~109 MiB actually in use, case 2 reports:
Proposed Solution
Total is already available and correct — keep using
info.MemTotalfromdockerClient.Info().Usage can come from the LXC's own cgroup, and no new bind mount is required, because
docker/examples/compose.basic.yamlalready shipscgroup: host. Withcgroupns=host,/sys/fs/cgroupinside the container is the LXC's cgroup root:The derivation is the part worth getting right.
memory.currentincludes page cache and overstates badly against/proc/meminfosemantics. Same 32 GiB LXC, all four numbers taken together:memory.currentmemory.current - inactive_file(Docker/cAdvisor convention)memory.current - fileSo
memory.current - file(frommemory.stat) is the one that matches whatfreeshows inside the LXC.One trap: do not try to read the total from that cgroup.
/sys/fs/cgroup/memory.maxat the LXC root readsmax— the real limit lives host-side at/sys/fs/cgroup/lxc/<vmid>/memory.maxand is invisible from inside the guest. The Docker API value is the correct source.For detecting "Docker inside LXC" versus "Docker on bare metal", one signal you already have: compare
info.MemTotalfrom the Docker API against the machine total gopsutil reports. On bare metal they agree; in an LXC the daemon reports the LXC's smaller limit while gopsutil reports the hypervisor's. I have not implemented or verified that heuristic, so treat it as a suggestion rather than a recommendation.Alternatives Considered
As a stopgap I publish the LXC's own
/proc/meminfoto a plain file from a small systemd unit inside the LXC (the read is taken by an LXC-level process, so lxcfs scopes it correctly) and bind it over Arcane's/proc/meminfo. That works today and gives exactly the target numbers:But it needs a refresher daemon, a compose bind, and carries up to one refresh interval of staleness. Reading the cgroup directly needs none of that, which is why I would rather see it in Arcane.
Arcane Version
v2.9.0
Installation Method
Docker Compose (Recommended)
Environment Type
Local Docker environment
Database Type
SQLite (default)
Operating System
Debian 13 (unprivileged LXC on Proxmox VE 9.2.10, lxcfs 7.0.0-pve1, Docker 29.7.2)
Additional Context
All figures above were measured on live hardware: unprivileged LXC,
features: nesting=1, 32768 MB / 6 cores on a 62 GiB host, plus a second 2048 MB LXC used for the stopgap verification.