1111# docker buildx build --build-context nemo-gym=. -f docker/Dockerfile \
1212# --tag <registry>/nemo-gym:latest --push .
1313#
14+ # Non-root runtime override (the image still defaults to root):
15+ # docker run --rm --user 65532:65532 \
16+ # --env HOME=/home/nemo-runtime \
17+ # --env XDG_CACHE_HOME=/opt/nemo-gym/cache/xdg \
18+ # <registry>/nemo-gym:latest --help
19+ #
1420
1521ARG BASE_IMAGE=nvcr.io/nvidia/cuda-dl-base:26.03-cuda13.2-devel-ubuntu24.04
1622
6874
6975ARG UV_VERSION=0.11.29
7076ARG PYTHON_VERSION=3.13.14
71- ENV PATH="/root/.local/bin:$PATH"
72- RUN curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh && \
73- uv python install ${PYTHON_VERSION}
77+ ENV UV_PYTHON_INSTALL_DIR=/opt/uv/python
78+ ENV UV_CACHE_DIR=/opt/uv/cache
79+ RUN curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | \
80+ env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh && \
81+ /usr/local/bin/uv python install ${PYTHON_VERSION}
82+ ENV PATH="/usr/local/bin:$PATH"
83+
84+ ARG RUNTIME_UID=65532
85+ ARG RUNTIME_GID=65532
86+ RUN if ! getent group "${RUNTIME_GID}" >/dev/null; then \
87+ groupadd --gid "${RUNTIME_GID}" nemo-runtime; \
88+ fi && \
89+ if ! getent passwd "${RUNTIME_UID}" >/dev/null; then \
90+ useradd --no-log-init --uid "${RUNTIME_UID}" --gid "${RUNTIME_GID}" \
91+ --create-home --home-dir /home/nemo-runtime --shell /bin/bash nemo-runtime; \
92+ fi && \
93+ install -d -o "${RUNTIME_UID}" -g "${RUNTIME_GID}" \
94+ /home/nemo-runtime \
95+ /opt/nemo-gym \
96+ /opt/nemo-gym/cache \
97+ /opt/nemo-gym/results \
98+ /opt/uv/cache
7499
75100ENV RAY_USAGE_STATS_ENABLED=0
76101
@@ -81,13 +106,16 @@ WORKDIR /opt/nemo-gym
81106
82107ARG BASE_IMAGE
83108ARG UV_VERSION
109+ ARG RUNTIME_UID
110+ ARG RUNTIME_GID
84111
85112ENV UV_PROJECT_ENVIRONMENT=/opt/nemo_gym_venv
86113ENV UV_LINK_MODE=copy
87114
88115# Copy only dependency metadata first for layer caching.
89- COPY --from=nemo-gym pyproject.toml uv.lock ./
90- COPY --from=nemo-gym nemo_gym/__init__.py nemo_gym/package_info.py ./nemo_gym/
116+ COPY --from=nemo-gym --chown=${RUNTIME_UID}:${RUNTIME_GID} pyproject.toml uv.lock ./
117+ COPY --from=nemo-gym --chown=${RUNTIME_UID}:${RUNTIME_GID} \
118+ nemo_gym/__init__.py nemo_gym/package_info.py ./nemo_gym/
91119
92120RUN uv venv --seed && \
93121 uv sync --link-mode symlink --locked --extra vllm --no-install-project
@@ -111,11 +139,19 @@ LABEL com.nvidia.build.ref="${NVIDIA_BUILD_REF}"
111139
112140# Copy full source. Exclude pyproject.toml and uv.lock since they are
113141# already present from the hermetic stage.
114- COPY --from=nemo-gym --exclude=pyproject.toml --exclude=uv.lock . /opt/nemo-gym
142+ COPY --from=nemo-gym --chown=${RUNTIME_UID}:${RUNTIME_GID} \
143+ --exclude=pyproject.toml --exclude=uv.lock . /opt/nemo-gym
115144
116145# Install the nemo-gym project itself along with the vllm extra.
117146RUN UV_LINK_MODE=symlink uv sync --locked --extra vllm
118147
148+ # Keep runtime uv and XDG caches separate from the root-owned build cache that
149+ # backs the symlinked project environment.
150+ ENV UV_CACHE_DIR=/opt/nemo-gym/cache/uv
151+ RUN install -d -o "${RUNTIME_UID}" -g "${RUNTIME_GID}" \
152+ "${UV_CACHE_DIR}" \
153+ /opt/nemo-gym/cache/xdg
154+
119155# Optional: pre-warm per-server venvs at build time so they are ready at runtime.
120156# Pass --build-arg NEMO_GYM_PREFETCH_CONFIGS="path/to/config1.yaml,path/to/config2.yaml"
121157# Uses 'gym env prefetch' which accepts the same config format as 'gym env start',
@@ -126,5 +162,53 @@ if [[ -n "${NEMO_GYM_PREFETCH_CONFIGS:-}" ]]; then
126162fi
127163EOF
128164
165+ # Prefetched caches and venvs are built as root. Own the project tree and the
166+ # project environment's directories so the runtime identity can mutate them.
167+ RUN venv_site_packages="$(/opt/nemo_gym_venv/bin/python -c \
168+ 'import site; print(site.getsitepackages()[0])')" && \
169+ touch "${venv_site_packages}/.nonroot-uninstall-probe" && \
170+ chown -R "${RUNTIME_UID}:${RUNTIME_GID}" /opt/nemo-gym && \
171+ find /opt/nemo_gym_venv -type d \
172+ -exec chown "${RUNTIME_UID}:${RUNTIME_GID}" {} +
173+
174+ USER ${RUNTIME_UID}:${RUNTIME_GID}
175+
176+ # Runtime callers select this UID and set HOME/cache variables explicitly; the
177+ # published image remains root by default. Prove that the CLI, uv toolchain,
178+ # project environment, source tree, and runtime paths support that contract.
179+ RUN export HOME=/home/nemo-runtime \
180+ XDG_CACHE_HOME=/opt/nemo-gym/cache/xdg && \
181+ test "$(id -u)" = "${RUNTIME_UID}" && \
182+ test -w "${HOME}" && \
183+ test -w "${XDG_CACHE_HOME}" && \
184+ test -w "${UV_CACHE_DIR}" && \
185+ test -w /opt/nemo-gym/cache && \
186+ test -w /opt/nemo-gym/results && \
187+ test -x /usr/local/bin/uv && \
188+ test -x /usr/local/bin/uvx && \
189+ test -x /opt/nemo_gym_venv/bin/python && \
190+ venv_site_packages="$(python -c 'import site; print(site.getsitepackages()[0])')" && \
191+ rm "${venv_site_packages}/.nonroot-uninstall-probe" && \
192+ touch "${HOME}/.nonroot-write-probe" \
193+ "${XDG_CACHE_HOME}/.nonroot-write-probe" \
194+ "${UV_CACHE_DIR}/.nonroot-write-probe" \
195+ /opt/nemo-gym/.nonroot-write-probe \
196+ /opt/nemo-gym/cache/.nonroot-write-probe \
197+ /opt/nemo-gym/results/.nonroot-write-probe \
198+ "${venv_site_packages}/.nonroot-write-probe" && \
199+ gym --help >/dev/null && \
200+ uv venv /opt/nemo-gym/cache/container-smoke && \
201+ /opt/nemo-gym/cache/container-smoke/bin/python -c \
202+ "import sys; assert sys.version_info[:2] == (3, 13)" && \
203+ rm -r /opt/nemo-gym/cache/container-smoke \
204+ "${HOME}/.nonroot-write-probe" \
205+ "${XDG_CACHE_HOME}/.nonroot-write-probe" \
206+ "${UV_CACHE_DIR}/.nonroot-write-probe" \
207+ /opt/nemo-gym/.nonroot-write-probe \
208+ /opt/nemo-gym/cache/.nonroot-write-probe \
209+ /opt/nemo-gym/results/.nonroot-write-probe \
210+ "${venv_site_packages}/.nonroot-write-probe"
211+
212+ USER root
129213ENTRYPOINT ["gym" ]
130214CMD ["--help" ]
0 commit comments