MILAB-6263: fix renv runtime permission-denied on non-root containers - #45
Conversation
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>
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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
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>
- 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>
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.rlangbrought in by ther-base:4.4.2base image) and failed withPermission denied.Already merged in
differential-clonotype-abundance(platforma-open/differential-clonotype-abundance#43); the sameDockerfilepattern 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 standardR_LIBS_USERenv var.RUN mkdir -p \"\${R_LIBS_USER}\"because renv silently falls back to the default library if the path doesn't exist./app/run.shand theENTRYPOINT; the platforma SDK passes the fullcmd(Rscript /app/...) at run time, so no wrapper is needed.Test plan
cannot create directory '/usr/local/lib/R/site-library/...': Permission denied.🤖 Generated with Claude Code
Greptile Summary
This PR fixes a
Permission deniedcrash that occurred when renv's runtimerestore()call tried to back up root-owned system library entries (e.g.rlangin/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.R_LIBS_USERandRENV_PATHS_LIBRARYto/app/renv/library(a writable,/app-scoped path), creates the directory explicitly, performs a two-pass build-time restore (parallel first, then sequentialclean=TRUEto resolve race conditions), and drops/app/run.sh+ENTRYPOINTsince the SDK injects the command directly at runtime.RENV_CONFIG_REPOS_OVERRIDEfromcloud.r-project.orgto 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.@platforma-sdk/block-toolsfrom~2.6.30to~2.7.24with 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
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)Reviews (1): Last reviewed commit: "MILAB-6263: pin CRAN to PPM dated snapsh..." | Re-trigger Greptile