Reproducible Environments with renv

· 5 min read · Updated March 11, 2026 · beginner
r renv reproducibility dependencies packrat workflow

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, something about 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.

This is dependency hell, and renv is the solution.

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 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. This is the magic that makes renv work without you thinking about it.

Once you’ve finished working, take a snapshot to record your current environment:

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")

To remove one:

remove.packages("ggplot2")

To update packages:

renv::update()

The renv::update() function checks all packages in your project 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.

You can also install packages directly through renv, which supports more sources than base R:

renv::install("tidyverse/dplyr")  # from GitHub
renv::install("Bioconductor/DESeq2")  # from Bioconductor

This is useful when you need packages not on CRAN or specific development versions.

To see what packages your project depends on, use:

renv::dependencies()

This scans your R scripts and identifies which packages you actually use, excluding packages that are installed but not referenced in your code.

You can also query the library directly:

renv::library()

This lists all packages currently 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 renv
  • renv/activate.R — the renv bootstrapper
  • renv/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 experience is seamless.

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.

Here’s an example for GitHub Actions:

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.

It doesn’t manage the R version itself. 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.

It doesn’t handle system dependencies like Pandoc, which rmarkdown requires. For complex setups, consider Docker — renv works well inside Docker containers.

It doesn’t guarantee binary compatibility. 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.

See Also