chore(docker): keep the build toolchain out of the runtime image and drop root - #64
Open
idapixl wants to merge 2 commits into
Open
chore(docker): keep the build toolchain out of the runtime image and drop root#64idapixl wants to merge 2 commits into
idapixl wants to merge 2 commits into
Conversation
…drop root Two hardening fixes to the published image. **The compiler shipped to production.** The runtime stage installed python3, make and g++ so `npm ci --omit=dev` could rebuild better-sqlite3, and never removed them — so every published image carried a full C++ toolchain. Purging them in a later RUN would not have helped: image layers are additive, so the packages remain inside the earlier layer even after being removed. Production dependencies are now compiled in their own `prod-deps` stage and the runtime stage copies the finished node_modules, so it installs no compiler at all. Both stages share node:24-slim, so the native binary matches. **The container ran as root.** Now drops to the unprivileged `node` user (uid 1000) that the base image already ships. The chown is load-bearing, not decoration. The SQLite store defaults to a relative `./cortex.db` (core/config.ts), which resolves to /app at runtime. Everything copied in is owned by root, so dropping to `node` without chowning /app would turn the default configuration into a startup failure — the exact way a non-root switch usually regresses. Verified: the image builds in CI. Not verified locally — Docker was not available in the environment this was prepared in — so the runtime path deserves a smoke test before this is relied on: start the container and confirm it serves on 8080 and can create cortex.db. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It changes production container runtime permissions and filesystem ownership behavior (not runtime-smoke-tested per PR description), which warrants a human-verified run check before approval.
Pull request overview
Updates the container build to keep the compiler toolchain out of the shipped runtime image and to run the service as an unprivileged user, reducing the production image attack surface and aligning runtime permissions with the SQLite default DB path.
Changes:
- Split production dependency installation into a dedicated
prod-depsstage that includes the native build toolchain, then copy the resultingnode_modulesinto the runtime stage. - Drop root privileges in the runtime stage by switching to the built-in
nodeuser after ensuring/appis writable for the default./cortex.dblocation.
File summaries
| File | Description |
|---|---|
| Dockerfile | Introduces a prod-deps stage for native addon compilation and switches the runtime stage to non-root execution with a writable /app. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # resolves to /app at runtime — so /app must be writable by the running user. | ||
| # Without this chown, dropping to `node` turns the default configuration into | ||
| # a startup failure. | ||
| RUN chown -R node:node /app |
idapixl
enabled auto-merge (squash)
September 7, 2026 21:26
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.
The last two items from the #58/#60 audit backlog.
1. The C++ toolchain shipped to production
The runtime stage installed
python3,makeandg++sonpm ci --omit=devcould rebuildbetter-sqlite3— and never removed them. Every published image carried a full compiler toolchain.Purging them in a later
RUNwould not have fixed it: image layers are additive, so the packages stay inside the earlier layer even after removal. The only real fix is never installing them in that stage.Production dependencies now compile in their own
prod-depsstage, and runtime copies the finishednode_modules. Both stages sharenode:24-slim, so the compiled native binary matches.2. The container ran as root
Now drops to the unprivileged
nodeuser (uid 1000) the base image already ships.The
chownis load-bearing, not decoration. The SQLite store defaults to a relative./cortex.db(core/config.ts), which resolves to/appat runtime. Everything copied in is owned by root, so dropping tonodewithout chowning/appwould turn the default configuration into a startup failure — the exact way a non-root switch usually regresses.Verification status — please read
docker-publish.ymlrunsbuild-and-pushon PRs withpush: false, so this PR proves the image builds.CI proves it compiles, not that it runs. Before relying on this, smoke-test:
The failure mode to watch for is a permission error on
cortex.dbcreation. If the deployment mounts a volume over/app, the mount's ownership wins over the imagechownand will need--user 1000or an explicitchownon the host path.