Skip to content

chore(docker): keep the build toolchain out of the runtime image and drop root - #64

Open
idapixl wants to merge 2 commits into
masterfrom
chore/harden-dockerfile
Open

chore(docker): keep the build toolchain out of the runtime image and drop root#64
idapixl wants to merge 2 commits into
masterfrom
chore/harden-dockerfile

Conversation

@idapixl

@idapixl idapixl commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

The last two items from the #58/#60 audit backlog.

1. The C++ toolchain shipped to production

The runtime stage installed python3, make and g++ so npm ci --omit=dev could rebuild better-sqlite3 — and never removed them. Every published image carried a full compiler toolchain.

Purging them in a later RUN would 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-deps stage, and runtime copies the finished node_modules. Both stages share node:24-slim, so the compiled native binary matches.

2. The container ran as root

Now drops to the unprivileged node user (uid 1000) 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.

Verification status — please read

  • Build: verified by CI. docker-publish.yml runs build-and-push on PRs with push: false, so this PR proves the image builds.
  • Runtime: not verified. Docker was unavailable in the environment this was prepared in.

CI proves it compiles, not that it runs. Before relying on this, smoke-test:

docker run --rm -p 8080:8080 <image>
# confirm it serves on 8080 and can create cortex.db as uid 1000

The failure mode to watch for is a permission error on cortex.db creation. If the deployment mounts a volume over /app, the mount's ownership wins over the image chown and will need --user 1000 or an explicit chown on the host path.

…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>
Copilot AI lite review requested due to automatic review settings September 7, 2026 21:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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-deps stage that includes the native build toolchain, then copy the resulting node_modules into the runtime stage.
  • Drop root privileges in the runtime stage by switching to the built-in node user after ensuring /app is writable for the default ./cortex.db location.
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.

Comment thread Dockerfile
# 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
idapixl enabled auto-merge (squash) September 7, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants