Skip to content

fix(docker): actually run all seven servers as their non-root user - #4783

Open
Ashfaqbs wants to merge 1 commit into
modelcontextprotocol:mainfrom
Ashfaqbs:fix/dockerfiles-run-as-non-root
Open

fix(docker): actually run all seven servers as their non-root user#4783
Ashfaqbs wants to merge 1 commit into
modelcontextprotocol:mainfrom
Ashfaqbs:fix/dockerfiles-run-as-non-root

Conversation

@Ashfaqbs

@Ashfaqbs Ashfaqbs commented Sep 9, 2026

Copy link
Copy Markdown

Description

Since #2205, the three Python Dockerfiles (fetch, git, time) create an app user and --chown the copied venv to it, but never actually switch to it with USER — so those lines are dead code and every container still runs as UID 0. The four Node images (everything, filesystem, memory, sequentialthinking) inherit root from node:22-alpine and never use the non-root node user that base image already ships.

Adding USER app/USER node isn't sufficient by itself, though — I found two things that would otherwise break these servers the moment they stop running as root:

  1. /root defaults to 700. Each Python venv's python binary is a symlink into /root/.local/share/uv/python/.../bin/python3.11, resolved at every process startup, not just at build time. A non-root user can't even traverse into /root to reach it, so the interpreter itself fails (Could not find platform independent libraries...) — not just a file-permission error somewhere incidental. Fixed with chmod o+rx /root before the USER switch.
  2. git now runs as a different uid than whatever repo gets bind-mounted in. Modern git refuses to operate on a directory it doesn't consider "safe" when the owning uid doesn't match the calling user. Since this container's only job is operating on whatever repo is mounted into it, there's no meaningful "wrong owner" to protect against here, so I added git config --system --add safe.directory '*'.

memory gets one more line: its default MEMORY_FILE_PATH is dist/memory.jsonl, right next to the compiled entrypoint — dist/ was just copied in as root, so I chown -R node:node /app/dist before the switch, otherwise the very first write to the default path fails.

Fixes #4741

Server Details

  • Server: fetch, git, time, everything, filesystem, memory, sequentialthinking
  • Changes to: Dockerfile only (no tool/resource/prompt changes)

Motivation and Context

Containers built from these Dockerfiles run as root despite already carrying most of the scaffolding to run as non-root, which fails Kubernetes runAsNonRoot admission policies outright and, for filesystem, makes write_file/create_directory create host files the invoking user then can't edit without sudo.

How Has This Been Tested?

Built and ran the three Python images directly (fetch, git, time) — confirmed id reports the non-root user in each, and drove a real MCP initialize handshake over stdio against each one to confirm the server actually starts and responds (not just "doesn't crash immediately"). For git specifically, went further: bind-mounted a real host repo (owned by a different uid than the container's app user, as it always will be on a real machine) and called the git_status tool through a full MCP round-trip — confirmed it correctly reports the working tree, proving the safe.directory fix actually resolves the dubious-ownership rejection rather than just not crashing on startup.

I could not build-test the four Node images locally — npm install in their builder stage currently fails on unmodified main too (confirmed via a from-scratch git archive build with a fully cleared build cache), with Cannot read properties of null (reading 'edgesOut'); filed separately as #4782 with a bisection down to the devDependencies set, since it isn't something this PR touches or introduces. For those four, USER node mirrors the identical pattern verified working on the Python images and is the pattern Node's own official images document; hadolint reports no new errors or warnings against any of the seven Dockerfiles from this change (only the expected informational note about node/app being a name rather than a numeric UID).

Breaking Changes

None for typical usage. Anyone relying on the (undocumented, unintended) root behavior — e.g. writing outside directories the non-root user can reach — would need to adjust, but that's exactly the behavior #4741 asks to close.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly (no README documents the root/non-root behavior today, so there's nothing to update)
  • I have tested this with an LLM client (real MCP initialize/tool-call round trips, described above)
  • My code follows the repository's style guidelines
  • New and existing tests pass locally (no test suite covers Dockerfiles; verified via direct build+run instead, see above)
  • I have added appropriate error handling (n/a — Dockerfile-only change)
  • I have documented all environment variables and configuration options (none added)

Additional context

Filed #4782 for the unrelated npm install failure this surfaced while verifying the Node images.

🤖 Generated with Claude Code

The three Python images (fetch, git, time) create an app user and
--chown the venv to it, but never switch to it with USER, so the
useradd/--chown lines were dead and every process still ran as root.
The four Node images inherit root from node:22-alpine despite that
base image already shipping a non-root node user.

Adding USER app/USER node alone isn't enough, though: /root defaults
to 700, and each Python venv's interpreter is a symlink into
/root/.local/share/uv/python/.../bin, resolved on every startup, not
just at build time — so switching users without loosening that
permission breaks the interpreter outright. Fixed by chmod o+rx /root
before the USER switch, verified by actually building and running all
three Python images (git also needed a wildcard safe.directory, since
git now runs as a different uid than whatever host directory gets
bind-mounted in).

Verified by building and exercising each image (id, plus a real MCP
initialize handshake; for git, a real git_status against a bind-mounted
repo owned by a different uid). The four Node images could not be
build-tested locally due to an unrelated, pre-existing npm install
failure reproducible on main (filed as modelcontextprotocol#4782); the USER node addition
mirrors the exact fix already verified working on the Python images
and matches the pattern Node's own official images document, and for
memory specifically also chowns dist/ first since that's where the
knowledge graph JSONL persists by default.

Fixes modelcontextprotocol#4741
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.

Dockerfiles for fetch/git/time create an app user (since #2205) but never switch to it; all seven images run as root

1 participant