fix(docker): drop Swarm-oriented deploy: keys so the stack deploys on NAS platforms - #108
Merged
Merged
Conversation
… NAS platforms
Synology Container Manager and QNAP Container Station reject or mishandle
`deploy:` keys, so docker-compose.yml would not deploy there at all, even though
Compose v2 honours the block standalone. All three services carried a
deploy.resources block (and a fourth copy sat in the README's paste-in stack).
Memory limits stay on by default, re-expressed with the portable mem_limit /
mem_reservation keys: scrye 2g + 256m reserved, trivy-server 1g,
docker-socket-proxy 64m. Memory is the containment control that matters most —
it bounds the OOM blast radius, and the RAM-backed /tmp tmpfs is charged
against it.
CPU limits move to a new opt-in overlay, docker/docker-compose.cpu-limits.yml,
rather than being deleted; the caps are a hardening measure and the 0.5 cap on
the socket proxy (the only container mounting docker.sock) is deliberate. The
overlay uses the portable `cpus:` key, not `deploy:` — Compose rejects a merged
project whose overlay contributes deploy.resources.limits against the base
file's mem_limit ("can't set distinct values on 'mem_limit' and
'deploy.resources.limits.memory'"), so a deploy-based overlay would not apply.
`cpus:` is the same limit, not a weaker one: Compose refuses a project that sets
`cpus` and `deploy.resources.limits.cpus` to different values, which a
parsed-and-ignored key could not do.
Verified with `docker compose config` across base-alone and base+overlay, each
with and without the sidecar profiles; the merged config reproduces the
pre-change limits exactly. Adds five regression guards to
test_compose_hardening.py covering both halves of the split.
See docs/ARCHIVE.md § Deviations for changes made in this pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A user reported that the
deploy:resource limits prevent deployment on their NAS. Synology Container Manager and QNAP Container Station reject or mishandle the Swarm-orienteddeploy:block, sodocker/docker-compose.ymlwould not deploy there at all — even though Compose v2 honours it standalone.The limits are a CIS-baseline hardening measure, so they were re-expressed rather than removed.
What was there
deploy:appeared on all three services, only ever asresources(noreplicas,restart_policy,placement,update_config). A fourth copy sat in the README's paste-in stack.limits.cpuslimits.memoryreservations.memoryscrye"2.0"2G256Mtrivy-servertrivy-server"1.0"1Gdocker-socket-proxydocker-env"0.5"64MWhat changed
Memory limits stay on by default, written with the portable
mem_limit/mem_reservationkeys that every Compose implementation accepts. Memory is the containment control that matters most here: it bounds the OOM blast radius, and the RAM-backed/tmptmpfs is charged against it. No opt-in required.CPU limits moved to a new opt-in overlay,
docker/docker-compose.cpu-limits.yml, applied with a second-f:docker compose -f docker/docker-compose.yml \ -f docker/docker-compose.cpu-limits.yml up -dThey were moved rather than deleted: the caps are tuned, and the
0.5ondocker-socket-proxyis deliberate — that sidecar is the only container in the stack that mountsdocker.sock, and its cap is the documented bound on a wedged or runaway proxy. CPU exhaustion degrades where memory exhaustion kills, which is what makes CPU the right half to make opt-in.Two things the implementation turned up
The overlay can't use
deploy:. The first draft did, on the reasoning that opting in implies a platform that supports it.docker compose configrejected the merged project:Compose normalizes
mem_limitanddeploy.resources.limits.memoryinto one field and validates that they agree, so an overlay contributing adeploy.resources.limitsmap with nomemoryin it conflicts with the base file'smem_limit. The overlay would not have applied at all. The portablecpus:key merges cleanly and is accepted by more implementations besides.cpus:is the same limit, not a weaker one — verified rather than assumed. Compose enforces the identical normalization for CPU: a service setting bothcpus: 2.0anddeploy.resources.limits.cpus: "3.0"is rejected withcan't set distinct values on 'cpus' and 'deploy.resources.limits.cpus', and setting both to the same value normalizes to one limit. A key that were merely parsed and ignored could not participate in that check.Verification
docker compose configwas run on all four combinations — base alone and base + overlay, each with no profiles and with--profile trivy-server --profile docker-env. All four parse, and the merged config reports exactly the pre-change values:Only the Compose CLI is available in this environment (no daemon), so no container was started — what is verified is that the files parse and normalize to the original limits, not that a live container was inspected.
docker compose up//healthzis unverified here for that reason; CI's image job is the gate.Five regression guards were added to
backend/tests/test_compose_hardening.py: nodeploy:key in either file, amem_limitfor every service,scrye'smem_reservation, acpusentry in the overlay for every service the base file defines, and the overlay's use of the portable key. They were exercised against seven mutations of the real files (adeploy:block returning, the overlay deleted,mem_limit/mem_reservationdropped, a new uncapped service, the overlay reverting todeploy:, the socket-proxy cap loosened) — each fires on the regression it names, with the unmodified files clean. They are string-level like the rest of that module, deliberately: the backend declares no YAML parser.The backend suite could not be run locally (this container has Python 3.11; the backend requires 3.14), so the new test bodies were executed standalone against the checked-in files.
ruffandblackare clean.Docs
COMPOSE_FILEalternative, the warning that both-fflags must be repeated on every command for the stack, and a pointer for NAS users to their platform's own CPU controls. The paste-in stack, the security-model bullet, the sidecar commands, and the contents list are updated to match.CHANGELOG.mdunder Unreleased, with the action-required note.docs/ARCHIVE.md§ Deviations for changes made in this pass.