Reproducible Environments with renv
Every R programmer has been there: you write code that works perfectly on your machine, send it to a colleague, and it breaks immediately. The error message is cryptic — a missing function or a version mismatch. You spend hours debugging, only to discover they have an older version of a package where the API was different.
Creating reproducible environments with renv solves this problem entirely. renv captures the exact versions of every package your project uses so anyone can reconstruct your working setup on any machine.
What renv solves
R’s default behavior is to dump all packages into a shared system library. When you run install.packages(dplyr), it updates the global installation. If you’re working on multiple projects with different requirements, this becomes a problem. Project A needs dplyr 1.0.0, but Project B requires dplyr 1.1.0. You can’t have both installed simultaneously in the global library.
renv creates isolated project libraries. Each project gets its own directory containing exactly the package versions it needs. Your dplyr 1.0.0 project lives in its own world, completely separate from the dplyr 1.1.0 project. They never interfere with each other.
Beyond isolation, renv records exactly which packages and versions your project uses. You can share this record with collaborators or your future self, and recreate the exact same environment on any machine.
Getting started
Install renv from CRAN like any other package:
install.packages("renv")
Initialize renv in your project by calling renv::init(). This command scans your project directory, creates a dedicated package library, and writes the initial lockfile. After initialization completes, your project has its own private library completely separate from your system R installation, so package installs stay local to the project:
renv::init()
This does three things. First, it creates a project library at renv/library/, this is where your project’s packages will live. Second, it generates a lockfile called renv.lock that records all package metadata. Third, it modifies your .Rprofile to configure R to use the project library instead of the system library.
After running renv::init(), you’ll see new files in your project directory. The .Rprofile ensures that every time you open this project in R, it automatically uses the project-specific library, which makes renv work without requiring you to remember to activate it manually. Once you have finished making changes to your environment, record them with a single snapshot command:
renv::snapshot()
This updates renv.lock with the exact versions of every package in your project library. If you add a new package, remove one, or upgrade to a newer version, calling snapshot() records that change.
When you or someone else needs to recreate the environment, run:
renv::restore()
This reads the lockfile and installs exactly the recorded versions. Your colleague can clone your project, run restore(), and have an identical environment.
Managing dependencies
renv is designed to work with your existing workflow. You don’t need to learn new package management commands. Just keep using install.packages() like normal, renv intercepts these calls and routes them to the project library.
To install a package:
install.packages("ggplot2")
When a package is no longer needed, removing it from the project library works the same way as in base R, which means you don’t have to learn a new API. The command is identical whether renv is active or not — renv captures the fact that a package was removed when you next call snapshot():
remove.packages("ggplot2")
To update all packages to their latest available versions, renv provides its own update command that checks every package in your library against their available sources and installs newer versions. After updating, test that your code still works, then run snapshot() to record the new versions:
renv::update()
For packages that are not on CRAN, renv supports installing directly from GitHub repositories and Bioconductor. This is essential when you rely on development versions of packages, bioinformatics tools hosted only on Bioconductor, or any non-CRAN source that you need pinned in your lockfile for reproducibility:
renv::install("tidyverse/dplyr") # from GitHub
renv::install("Bioconductor/DESeq2") # from Bioconductor
GitHub and Bioconductor installs are essential when you need packages outside CRAN or specific development versions. To understand which packages your project actually uses, renv can scan your R scripts and identify every package referenced in your code, helpful for cleaning up imports that are no longer needed or verifying that every dependency is recorded:
renv::dependencies()
The scan examines your R scripts and identifies which packages you actually use, excluding packages that are installed but not referenced in your code. The output is a data frame showing each package, the file it was found in, and how it was referenced (::, library(), or require()). For a different perspective, you can list every package physically present in your project library with a single command:
renv::library()
Calling renv::library() lists all packages currently installed in your project library.
How the lockfile works
The lockfile (renv.lock) is a JSON file that contains everything needed to recreate your environment. It records the R version, repositories, and every package with its version, source, and hash.
Here’s a simplified example:
{
"R": {
"Version": "4.4.1",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cloud.r-project.org"
}
]
},
"Packages": {
"dplyr": {
"Package": "dplyr",
"Version": "1.1.4",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "abc123def456"
}
}
}
The lockfile captures where each package came from (CRAN, GitHub, Bioconductor), the exact version, and a hash to verify integrity. This means it doesn’t just track the version number, it knows the exact installation source.
You should commit renv.lock to version control. This is what makes collaboration work: everyone pulls the same lockfile, runs restore(), and gets identical environments.
Sharing projects
When sharing code with collaborators, you need to commit several files to version control:
renv.lock, the lockfile with package versions.Rprofile, the project R profile that activates renvrenv/activate.R, the renv bootstrapperrenv/settings.json, renv configuration
You should NOT commit:
renv/library/, the actual package files (these can be huge)- Any large binary files
renv automatically generates a .gitignore that excludes the right files.
When a collaborator opens the project for the first time, renv bootstraps itself automatically. It downloads the appropriate version of renv, then prompts the user to run restore() to install all packages. The process requires no manual intervention.
CI/CD and automation
For automated workflows, you need to ensure renv restores packages in your CI environment. Most CI systems already have R installed, so you just need to run:
renv::restore()
at the start of your CI pipeline. This installs all packages from the lockfile before your tests run.
Restoring packages in CI ensures that your automated tests run against the exact same package versions you use locally. Here’s a complete example for GitHub Actions that checks out the code, sets up R, and restores from the lockfile before running tests:
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-r@v2
- name: Restore packages
run: renv::restore()
shell: Rscript {0}
- name: Run tests
run: testthat::test_dir("tests/")
This checks out your code, sets up R, restores packages from the lockfile, then runs tests.
What renv doesn’t do
renv handles R packages, but it doesn’t solve every reproducibility problem.
Managing the R version itself is outside renv’s scope. If your code requires R 4.3.0 and someone runs it on R 4.1.0, you may encounter issues. Tools like rig can help manage multiple R versions on one machine.
System dependencies like Pandoc, which rmarkdown requires, also fall outside renv’s management. For complex setups, consider Docker, renv works well inside Docker containers.
Binary compatibility is another gap: if you install a package from a precompiled binary on one system and try to restore it on another where only source installation is available, you may need system libraries to compile from source.
Caveats and limitations
renv manages R packages but not the R version itself. If your project requires R 4.3 and a team member runs R 4.1, renv::restore() cannot fix that mismatch. Document the required R version in README.md or use a .Rversion file (supported by some tools). For full environment reproducibility including the R binary, combine renv with Docker, using a Rocker base image that pins the R version.
System libraries (libssl, libcurl, libgdal, etc.) required by some R packages are also outside renv’s scope. pak::pkg_sysreqs() can generate the list of required system packages for a given set of R packages — document these in README.md or automate their installation in a Dockerfile.
See also
- Data Wrangling with dplyr: core dplyr verbs for data manipulation in reproducible projects
- Reading and Writing CSV Files in R: handle data I/O in your R projects
- Error Handling in R: write reliable R code that handles failures gracefully