Repository navigation
fix(deploy): pass capacity.env values through Compose and prepare runtime dirs - #249
Merged
ttbombadil merged 3 commits intoOct 12, 2026
Merged
Conversation
ttbombadil
marked this pull request as ready for review
October 12, 2026 15:53
…time dirs Two defects found on a fresh Ubuntu 26.04 host: - docker-compose.yml did not forward SIMULATION_QUEUE_TIMEOUT_MS and pinned SIMULATION_MAX_CONCURRENT to 200, so two of the seven capacity.env recommendations never reached the backend. Both are now read from .env. Defaults are unchanged: 200 for the simulation limit, and an empty queue timeout keeps the application default (60000 ms) from server/config.ts instead of introducing a second default. - The bind-mounted server/arduino-cache, temp and storage were root:root (created by Docker) or owned by the installing host user, so the backend user (node, UID/GID 1000) got EACCES on every compile. The new scripts/prepare-runtime-dirs.sh creates them and hands them to UID/GID 1000 with mode 0750; the installer calls it. It targets the container user, never the host UID, and the backend still does not run as root. The installer deployment test now prepares the directories with the real script instead of chmod 0777, asserts the backend can write them, and checks that .env capacity values reach /api/status. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nt test Sonar S4624 flagged the .env assembly added with the capacity pass-through assertions. Build the capacity lines first and concatenate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
prepare-runtime-dirs.sh hands server/arduino-cache, temp and storage to the container user (UID/GID 1000, mode 0750), which another host account cannot read. server/arduino-cache and temp were already ignored; storage was not. A build step that copies the whole context (or a directory containing the mount) then fails with "open .../storage: permission denied". With the current Dockerfile BuildKit does not read storage, so this is a safeguard rather than a repair, but storage is runtime state and never belongs in an image. The regression test builds `COPY . /all` with the real .dockerignore as UID 1001 in a Linux container against directories owned by 1000 with mode 0750, and checks that the same build fails once a runtime directory is not ignored. A static test ties the ignore list to the compose bind mounts. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ttbombadil
force-pushed
the
fix/compose-capacity-passthrough-and-runtime-dirs
branch
from
October 12, 2026 16:32
e2714f7 to
6cf1c99
Compare
ttbombadil
deleted the
fix/compose-capacity-passthrough-and-runtime-dirs
branch
October 12, 2026 16:54
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.
Summary
Fixes the two deployment defects found on a fresh Ubuntu 26.04 install after the calibration run itself had succeeded. No change to capacity policy, the security model, the gateway/trust semantics, or production defaults (with the one clearly marked exception below, where an existing value was not passed through at all).
Based on current
main(rebased after #248 was merged); the diff contains only the three commits below.3.
capacity.envvalues were not reaching the backend containerdocker-compose.ymldid not forwardSIMULATION_QUEUE_TIMEOUT_MSat all, so a calibrated151000was silently replaced by the application default (Queue timeout: 60 s).SIMULATION_QUEUE_TIMEOUT_MS=${SIMULATION_QUEUE_TIMEOUT_MS:-}: an empty value keeps the application default (60000 ms) fromserver/config.ts, which already treats an empty string as unset. No second default is introduced.SIMULATION_MAX_CONCURRENT=200was hard-coded althoughdocs/INSTALL_SERVER.mdlists it as an.envsetting and calibration proposes it. It is now${SIMULATION_MAX_CONCURRENT:-200}: the default stays 200, a reviewed value now takes effect. The other five variables ofcapacity.envwere already forwarded.INSTALL_SERVER.md,CAPACITY_MODEL.md) say thatcapacity.envlines only take effect once they are in.envand the backend container has been recreated, and where to verify them (/api/status→capacity).4. Bind-mounted runtime directories were not writable for the container user
The backend runs as
node(UID/GID 1000).server/arduino-cache,tempandstoragewereroot:root(created by Docker for a missing mount source) or owned by the installing host user (UID 1001 in the field), so every compile failed withEACCES→POST /api/compile 500.scripts/prepare-runtime-dirs.sh: creates the three directories and hands them to UID/GID 1000 (chown -hR, mode0750). It targets the container user, not the host UID. Nochmod 777, the backend still does not run as root, the container's security settings are untouched.root:root; refuses symlinks and non-directories before changing anything; requiressudounless the target already is the running account.scripts/install-ubuntu-vbox.shcalls it instead of a plainmkdir -pas the installing user.docs/INSTALL_SERVER.mdhas the one-timesudo ./scripts/prepare-runtime-dirs.shstep;docs/INSTALL_UBUNTU_VBOX.mddescribes the resulting ownership.Build context:
storageadded to.dockerignoreAfter the script ran, the host account can no longer read these directories.
server/arduino-cacheandtempwere already in.dockerignore;storagewas not.What I measured (Docker CLI as UID 1001 in a Linux container, directories
1000:1000mode0750):storagenot ignoredstorageignoredCOPYlines (BuildKit)COPY . /allopen /ctx/storage: permission deniedCOPY server /server(arduino-cacheinside the copied directory)open /ctx/server/arduino-cache: permission deniedSo with today's
DockerfileBuildKit never readsstorage, and the ignore is a safeguard, not a repair of an observed failure:storageis runtime state that never belongs in an image, and the build must keep working if a step ever copies the whole context. The existingserver/arduino-cacheentry is the one that matters today because that directory sits inside the copiedserver/. Two things the ignore does not do, found while testing: the legacy builder (DOCKER_BUILDKIT=0) and Docker Desktop for macOS stat every directory before applying excludes and fail on an unreadable one regardless of.dockerignore. Neither is the supported path (Ubuntu installer: BuildKit viadocker-buildx).How the failures were reproduced
On the fresh Ubuntu 26.04 / x86_64 host (as reported): after a successful calibration,
SIMULATION_QUEUE_TIMEOUT_MS=151000set in.envstill showedQueue timeout: 60 s; adding it to the compose environment locally showed151 s. Compile requests returned500withEACCESwhile the mount sources wereroot:root/ UID 1001; afterchown -R 1000:1000 server/arduino-cache temp storagecompile worked. I did not have access to that host.Locally: with the old
docker-compose.ymlthe new compose tests fail (SIMULATION_MAX_CONCURRENTpinned to200,SIMULATION_QUEUE_TIMEOUT_MSabsent); with the fix they pass. TheEACCESitself cannot be reproduced on macOS (Docker Desktop emulates bind-mount ownership), so it is covered by the Linux CI job below.Tests
tests/server/compose-capacity-passthrough.test.ts(new): the variable list is derived from thecapacity.envrenderer; realdocker compose configinterpolation checks that every variable set in.envlands in the backend environment and that defaults are unchanged (200 / 25 / empty queue timeout → application default).tests/server/prepare-runtime-dirs.test.ts(new): real filesystem permissions with the current account as target (never assumes UID 1000; the CI runner is 1001): ownership,0750, writable, no world access, idempotent, keeps content, refuses symlink/file, asks forsudoinstead of failing halfway, directory list equals the compose bind mounts.tests/server/docker-build-context.test.ts(new): every compose bind mount is in the real.dockerignoreand no negation re-includes one; theDockerfilecopies none of them; aCOPY . /allbuild as UID 1001 against1000:1000/0750directories succeeds with the real.dockerignoreand, as a control, fails withpermission deniedonce the runtime entries are removed. Needs the Docker daemon and thedocker:27-cliimage theDockerfilealready uses; skipped otherwise.scripts/deployment/installer-deployment.ts(npm run test:deployment:installer, CI job "Production Gateway Deployment"): replacedchmod 0o777on the runtime directories with the real script run as root in the server image, checks that the script's default UID:GID equalsidof the image's user, asserts the backend user (not root) can write all three paths, and thatSIMULATION_QUEUE_TIMEOUT_MS/SIMULATION_MAX_CONCURRENTappended to.envappear in/api/status.Verification
npm run check, ESLint--max-warnings=0,npm run check:docs,npm run build, and the full./run-tests.sh(static analysis, knip, unit, toolchain integration, Docker, Playwright E2E, production build, security audit, Sonar gate with 0 violations) on the final head; pre-push hook.prepare-runtime-dirs.sh, thendocker compose up --build -dwithSIMULATION_QUEUE_TIMEOUT_MS=151000in.env: readiness 200,/api/statusshowedqueue.timeoutMs: 151000andmaxConcurrent: 200(unchanged default),POST /api/compilereturned 200 asuid=1000(node).Dockerfilebuilt as host UID 1001 in a Linux container against directories prepared by the script (1000:1000,0750): image built.network_mode: hostis not reachable on Docker Desktop for Mac). This happens identically on unchangedmain. Everything the new code asserts before that step passed locally; the rest is exercised by the Linux CI job.Remaining risks
1000default is tied to thenodeuser of the base image; the deployment test fails if the image's user changes.0750and owner 1000, other host accounts cannot read the cache or temp contents (intentional: temp holds learners' sketches); administrators usesudo..dockerignore(see above); they are outside the supported Ubuntu path.tests/deployment/docker-compose.gateway.yml(separate gateway test topology, not the production compose file) stillchmod 0777s its own throwaway temp dir; left unchanged to keep this PR minimal.🤖 Generated with Claude Code