Description
With the systemd cgroup driver on cgroup v2, disabling swap by setting MemorySwap == Memory (Docker --memory 8g --memory-swap 8g) does not survive a systemctl daemon-reload.
ConvertMemorySwapToCgroupV2Value returns 0 for that case (memorySwap - memory). The two drivers then diverge:
The swap != 0 guard collapses "unset" and "explicitly zero" into the same case. Because systemd owns the scope, the next daemon-reload re-applies the unit's properties and rewrites memory.swap.max to max. The container silently regains unlimited swap; nothing on the container side changes (docker inspect still shows MemorySwap == Memory). docker update --memory X --memory-swap X restores 0 through the fs write, and the next reload resets it again.
This is not an exotic operator action: on 2026-09-05 an unattended kernel package upgrade triggered 15 daemon-reloads and every container on the host lost its swap ceiling.
Steps to reproduce
Ubuntu 24.04, systemd 255.4, cgroup v2, Docker 29.7.1 with runc 1.3.6 (vendors opencontainers/cgroups v0.0.4; the guard is unchanged on main), cgroupdriver=systemd.
$ docker run -d --name t --memory 256m --memory-swap 256m ubuntu sleep infinity
$ docker exec t cat /sys/fs/cgroup/memory.swap.max
0
$ sudo systemctl daemon-reload
$ docker exec t cat /sys/fs/cgroup/memory.swap.max
max
$ docker inspect -f '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' t
268435456 268435456
Observed on a long-running container: memory.swap.max went 0 -> max across a single reload. Recreating the container only repeats the transient fs write.
Expected behaviour
The systemd driver should express the explicit zero on the scope, the way fs2 does for the cgroup file, e.g. in genV2ResourcesProperties:
if swap != 0 || r.MemorySwap > 0 {
// swap == 0 && r.MemorySwap > 0: memory and memorySwap set to the same value -- disable swap
properties = append(properties, newProp("MemorySwapMax", uint64(swap)))
}
so that MemorySwapMax=0 is set and re-applied by systemd on every reload.
Workaround
A host-wide prefix drop-in /etc/systemd/system/docker-.scope.d/50-no-swap.conf with [Scope] / MemorySwapMax=0 makes systemd own the zero, but it applies to every container on the host regardless of its own swap request, so it is not a substitute for a fix.
Description
With the systemd cgroup driver on cgroup v2, disabling swap by setting
MemorySwap == Memory(Docker--memory 8g --memory-swap 8g) does not survive asystemctl daemon-reload.ConvertMemorySwapToCgroupV2Valuereturns0for that case (memorySwap - memory). The two drivers then diverge:fs2/memory.gospecial-cases it and writesmemory.swap.max=0:https://github.com/opencontainers/cgroups/blob/main/fs2/memory.go#L44-L55 (
// memory and memorySwap set to the same value -- disable swap)systemd/v2.gogenV2ResourcesPropertiesonly exports the property whenswap != 0, so the scope'sMemorySwapMaxstaysinfinitywhile the cgroup file says0:https://github.com/opencontainers/cgroups/blob/main/systemd/v2.go#L230-L237
The
swap != 0guard collapses "unset" and "explicitly zero" into the same case. Because systemd owns the scope, the nextdaemon-reloadre-applies the unit's properties and rewritesmemory.swap.maxtomax. The container silently regains unlimited swap; nothing on the container side changes (docker inspectstill showsMemorySwap == Memory).docker update --memory X --memory-swap Xrestores0through the fs write, and the next reload resets it again.This is not an exotic operator action: on 2026-09-05 an unattended kernel package upgrade triggered 15
daemon-reloads and every container on the host lost its swap ceiling.Steps to reproduce
Ubuntu 24.04, systemd 255.4, cgroup v2, Docker 29.7.1 with runc 1.3.6 (vendors opencontainers/cgroups v0.0.4; the guard is unchanged on
main),cgroupdriver=systemd.Observed on a long-running container:
memory.swap.maxwent0->maxacross a single reload. Recreating the container only repeats the transient fs write.Expected behaviour
The systemd driver should express the explicit zero on the scope, the way
fs2does for the cgroup file, e.g. ingenV2ResourcesProperties:so that
MemorySwapMax=0is set and re-applied by systemd on every reload.Workaround
A host-wide prefix drop-in
/etc/systemd/system/docker-.scope.d/50-no-swap.confwith[Scope]/MemorySwapMax=0makes systemd own the zero, but it applies to every container on the host regardless of its own swap request, so it is not a substitute for a fix.