Skip to content

MILAB-6263: fix renv runtime permission-denied on non-root containers - #45

Merged
blackcat merged 6 commits into
mainfrom
MILAB-6263-diff-abund-perm-denied
May 15, 2026
Merged

blackcat merged 6 commits into
mainfrom
MILAB-6263-diff-abund-perm-denied

Conversation

@blackcat

@blackcat blackcat commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Cross-block fix: the docker entrypoint of the R-based software re-ran renv::restore() on every container start. On hosts that run the container as a non-root UID (e.g. lab.research), renv tried to back up entries in the root-owned /usr/local/lib/R/site-library/ (e.g. rlang brought in by the r-base:4.4.2 base image) and failed with Permission denied.

Already merged in differential-clonotype-abundance (platforma-open/differential-clonotype-abundance#43); the same Dockerfile pattern was replicated here.

Change

  • ENV R_LIBS_USER=/app/renv/library + ENV RENV_PATHS_LIBRARY=\${R_LIBS_USER} so renv installs into a project-local library and R discovers packages via the standard R_LIBS_USER env var.
  • RUN mkdir -p \"\${R_LIBS_USER}\" because renv silently falls back to the default library if the path doesn't exist.
  • Drop /app/run.sh and the ENTRYPOINT; the platforma SDK passes the full cmd (Rscript /app/...) at run time, so no wrapper is needed.

Test plan

  • CI build of the docker image succeeds.
  • On a non-root host (lab.research), running the entrypoint no longer logs cannot create directory '/usr/local/lib/R/site-library/...': Permission denied.

🤖 Generated with Claude Code

Greptile Summary

This PR fixes a Permission denied crash that occurred when renv's runtime restore() call tried to back up root-owned system library entries (e.g. rlang in /usr/local/lib/R/site-library/) on hosts running containers as a non-root UID. The fix redirects renv's install target to a project-local, writable path and removes the runtime restore wrapper entirely.

  • Dockerfile: Sets R_LIBS_USER and RENV_PATHS_LIBRARY to /app/renv/library (a writable, /app-scoped path), creates the directory explicitly, performs a two-pass build-time restore (parallel first, then sequential clean=TRUE to resolve race conditions), and drops /app/run.sh + ENTRYPOINT since the SDK injects the command directly at runtime.
  • Repo pinning: Switches RENV_CONFIG_REPOS_OVERRIDE from cloud.r-project.org to a dated Posit Package Manager snapshot (2025-09-10) to lock the CRAN metadata to a pre-ggplot2-4.0 state and prevent silent lockfile-version drift.
  • Tooling: Bumps @platforma-sdk/block-tools from ~2.6.30 to ~2.7.24 with associated lockfile updates.

Confidence Score: 5/5

Safe to merge — the change correctly isolates renv's install target to a writable project-local path and removes the runtime restore wrapper that was the source of the permission failure.

The Dockerfile changes are well-reasoned: R_LIBS_USER and RENV_PATHS_LIBRARY both resolve to the same /app/renv/library path (Docker ENV interpolation expands the variable correctly at build time), the directory is created before renv runs, and the two-pass restore strategy handles parallel-install races without hiding genuine failures. Removing the ENTRYPOINT is intentional and consistent with how the SDK drives the container. The PPM dated snapshot pin prevents silent CRAN metadata drift. No logic errors or regressions are introduced.

No files require special attention. The pnpm-lock.yaml bump is a straightforward transitive dependency update alongside the block-tools version bump.

Important Files Changed

Filename Overview
software/Dockerfile Core fix: R_LIBS_USER and RENV_PATHS_LIBRARY now point to a writable project-local path (/app/renv/library); runtime renv::restore wrapper and ENTRYPOINT removed; two-pass build-time restore added; PPM dated snapshot replaces cloud.r-project.org
.changeset/fix-runtime-renv-restore-permission-denied.md Changeset entry documenting the fix; accurately describes root cause and solution
pnpm-workspace.yaml Bumps @platforma-sdk/block-tools catalog pin from ~2.6.30 to ~2.7.24
pnpm-lock.yaml Lockfile update matching the block-tools bump; several @milaboratories sub-packages updated as transitive dependencies

Sequence Diagram

sequenceDiagram
    participant Docker as Docker Build
    participant renv as renv::restore()
    participant lib as /app/renv/library
    participant SDK as Platforma SDK (runtime)
    participant Rscript as Rscript

    Docker->>renv: Pass 1 — parallel restore (tryCatch, errors logged)
    renv->>lib: Install packages to R_LIBS_USER
    Docker->>renv: "Pass 2 — sequential restore (Ncpus=1, clean=TRUE)"
    renv->>lib: Verify/fix any parallel-race installs

    Note over Docker,lib: Build complete — no ENTRYPOINT set

    SDK->>Rscript: Rscript /app/script.R
    Rscript->>lib: "Resolve packages via R_LIBS_USER=/app/renv/library"
    lib-->>Rscript: Packages found (writable path, non-root safe)
Loading

Reviews (1): Last reviewed commit: "MILAB-6263: pin CRAN to PPM dated snapsh..." | Re-trigger Greptile

Make renv install into a project-local library (/app/renv/library) and
point R at it via R_LIBS_USER, so the runtime wrapper can stop calling
renv::restore(). On hosts that run the container as a non-root UID, the
old wrapper hit Permission denied trying to back up entries in
/usr/local/lib/R/site-library/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request resolves a Permission denied error encountered when running the container as a non-root user by shifting the renv installation to a project-local library at /app/renv/library. The runtime renv::restore() call and the run.sh entrypoint wrapper have been removed to prevent unauthorized filesystem modifications at startup. Review feedback recommends extending the post-installation cleanup to include the new library path and explicitly setting executable permissions on R scripts to ensure reliability without the previous wrapper script.

Comment thread software/Dockerfile Outdated
Comment on lines 60 to 62
RUN R --no-echo -e "renv::restore(clean = TRUE)" \
&& find /root/.cache/R -name keys.html -delete \
&& find /usr/local/lib/R -name keys.html -delete

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since the R packages are now installed into a project-local library at ${R_LIBS_USER}, the cleanup logic should be extended to remove any keys.html files generated within that directory. Additionally, as the runtime wrapper script (run.sh) has been removed, it is recommended to explicitly ensure that the R scripts are executable. This is particularly important because they are invoked directly in the package.json command, and relying on source file permissions can be fragile.

RUN R --no-echo -e "renv::restore(clean = TRUE)" \
    && chmod +x /app/*.R \
    && find /root/.cache/R -name keys.html -delete \
    && find /usr/local/lib/R -name keys.html -delete \
    && find "${R_LIBS_USER}" -name keys.html -delete

pvyazankin and others added 4 commits May 12, 2026 13:20
CI 'require-latest' check fails when block-tools lags behind the latest
release. Bump the catalog pin so the lockfile resolves to 2.7.24.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l race

Single-pass renv::restore() with default Ncpus runs installs in parallel,
which races on dependencies like ggplot2 needing tibble first. PR #42 in
differential-clonotype-abundance fixed this with a two-pass: parallel
attempt (errors tolerated), then sequential rerun with clean=TRUE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use the same shape everywhere: union of apt deps (so the same Dockerfile
builds regardless of which R packages the block needs), two-pass renv::restore
to absorb parallel-install races, R_LIBS_USER project library, no runtime
wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
renv::restore with cloud.r-project.org doesn't fall back to /Archive/
when the locked package version is no longer the listed latest. It just
installs whatever /src/contrib/PACKAGES advertises. ggplot2 4.0.0 was
released on PPM ~Sep 15 2025; since then the lockfile's ggplot2 3.5.1
pin has been silently ignored and 4.0.x gets installed instead. 4.0.x
removed the internal ggplot2:::check_linewidth helper, which ggtree
3.14.0 calls during lazy-load — so the build fails.

Pinning RENV_CONFIG_REPOS_OVERRIDE to a PPM dated snapshot (frozen
package metadata) makes renv resolve the locked stack to a self-
consistent Bioc-3.20-era set (ggplot2 3.5.2 in this case — close enough
to the 3.5.1 pin and still has check_linewidth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@blackcat
blackcat marked this pull request as ready for review May 12, 2026 15:31
- apt deps sorted alphabetically
- additional `find "${R_LIBS_USER}" -name keys.html -delete` after the
  other two cache cleanups
- ensure trailing newline

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@blackcat
blackcat added this pull request to the merge queue May 15, 2026
Merged via the queue into main with commit 69c4ea1 May 15, 2026
8 checks passed
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