Renv with Docker: How to Dockerize a Shiny Application with an Reproducible Environment

[This article was first published on Tag: r - Appsilon | Enterprise R Shiny Dashboards, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

You might have seen our previous articles on Dockerizing R scripts and R Shiny applications and were left wondering: Are all of my dependency versions really fixed right now? It’s a good question, and the answer is both yes and no at the same time. Your dependencies are fixed, but their version might change in the future, which might cause unwanted behavior and crashes.

The solution? Combining renv with Docker. The renv package is pretty well-known in the R community, and it creates project-specific packages and a list of dependencies with their versions, making it easy to share your project.

Today you’ll learn how to combine the best of both worlds or how to use renv with Docker.

Need a refresher in R renv? Our guide for easy dependency management in R projects has you covered.

Table of contents


What is R renv and How it Works with Docker?

Renv stands for Reproducible Environment and is here to address the pain points many R developers have – managing environment dependencies.

By default, R installs packages to a central library and shares them between projects. This means you don’t have to install the dependencies again when working on a new project, but might introduce dependency version conflict when two or more developers are working on the same code.

This typically results in a lot of crashes and bugs that occur only on one machine. The reason is always the same – environment differences and package mismatch.

What renv allows you to do is to create a separate, reproducible environment that everyone can use.

But what does Docker have to do with renv? Good question. Docker is a platform for developing, shipping and running applications in isolated environments on an OS level. It’s kind of like a virtual machine but stripped down to bare bones.

Combining renv with Docker means you’ll be able to control the exact operating system your R code runs on along with system-wide dependencies, and you’ll also be able to control R version and R package dependencies.

It’s a win-win configuration for ultimate reproducibility and portability control over your R projects.

Do you need more introduction-level resources for R and Docker? Check out these articles:

Creating an R Shiny Application with renv

Creating and configuring a new R Shiny application with renv will take a couple of steps. You’ll have to initialize a new RStudio project, install dependencies, and create an environment snapshot before even writing a single line of R code.

Creating a New Project with renv

To start, open RStudio and create a new project. The easiest way is to use the project wizard.

Specify the path to where you want to save this new project, and make sure to tick the “Use renv with this project” checkbox. This will create all the files and folders needed to move forward:

Image 1 - Creating a new project in RStudio

Image 1 – Creating a new project in RStudio

As soon as the new project is initialized, you’ll see the following output in the R console:

Image 2 - Console output

Image 2 – Console output

And if you were to navigate to the project folder, you’d see a bunch of R environment files:

Image 3 - Directory structure created by renv

Image 3 – Directory structure created by renv

Basically, you now have an isolated R environment free of any external dependencies. Just what you want. The next step is to install the dependencies needed to run our app.

Installing R Dependencies with renv

We’ll create a simple R Shiny dashboard a bit later in the article, and it will rely on four external dependencies, so make sure to install them:

install.packages(c("shiny", "dplyr", "ggplot2", "gapminder"))

You’ll see the output that the packages are downloading:

Image 4 - Installing R packages

Image 4 – Installing R packages

It will take a couple of minutes to download all dependencies and sub-dependencies, and you’ll be asked for an installation confirmation when the download is complete:

Image 5 - Confirming the installation in a renv folder

Image 5 – Confirming the installation in a renv folder

Simply type “Y” and the packages will be installed in your isolated environment:

Image 6 - Package installation success message

Image 6 – Package installation success message

The packages are now installed, but the R environment isn’t aware of them. We’ll have to write the R Shiny application first, and then create an environment snapshot.

Coding the R Shiny application

The Shiny app code will feel familiar if you’ve followed our previous articles on R and Docker. Put simply, it’s an app that allows to user to change the continent (dropdown menu), and the two resulting charts change automatically.

The app uses the Gapminder dataset to show the shift in average life expectancy and average GDP per capita over time, summarized on all countries belonging to a continent.

Take extra notes on the call to options() function. We use it to hardcode the host and port values on which the Shiny application will run. This will make the deployment process easier later on.

Anyhow, here’s the full source code for our Shiny app (saved as app.R):

library(shiny)
library(dplyr)
library(ggplot2)
library(gapminder)

# Specify the application port
options(shiny.host = "0.0.0.0")
options(shiny.port = 8180)


ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      tags$h4("Gapminder Dashboard"),
      tags$hr(),
      selectInput(inputId = "inContinent", label = "Continent", choices = unique(gapminder$continent), selected = "Europe")
    ),
    mainPanel(
      plotOutput(outputId = "outChartLifeExp"),
      plotOutput(outputId = "outChartGDP")
    )
  )
)

server <- function(input, output, session) {
  # Filter data and store as reactive value
  data <- reactive({
    gapminder %>%
      filter(continent == input$inContinent) %>%
      group_by(year) %>%
      summarise(
        AvgLifeExp = round(mean(lifeExp)),
        AvgGdpPercap = round(mean(gdpPercap), digits = 2)
      )
  })

  # Common properties for charts
  chart_theme <- ggplot2::theme(
    plot.title = element_text(hjust = 0.5, size = 20, face = "bold"),
    axis.title.x = element_text(size = 15),
    axis.title.y = element_text(size = 15),
    axis.text.x = element_text(size = 12),
    axis.text.y = element_text(size = 12)
  )

  # Render Life Exp chart
  output$outChartLifeExp <- renderPlot({
    ggplot(data(), aes(x = year, y = AvgLifeExp)) +
      geom_col(fill = "#0099f9") +
      geom_text(aes(label = AvgLifeExp), vjust = 2, size = 6, color = "#ffffff") +
      labs(title = paste("Average life expectancy in", input$inContinent)) +
      theme_classic() +
      chart_theme
  })

  # Render GDP chart
  output$outChartGDP <- renderPlot({
    ggplot(data(), aes(x = year, y = AvgGdpPercap)) +
      geom_line(color = "#f96000", size = 2) +
      geom_point(color = "#f96000", size = 5) +
      geom_label(
        aes(label = AvgGdpPercap),
        nudge_x = 0.25,
        nudge_y = 0.25
      ) +
      labs(title = paste("Average GDP per capita in", input$inContinent)) +
      theme_classic() +
      chart_theme
  })
}

shinyApp(ui = ui, server = server)

This is what the app looks like when you run it:

Image 7 - Custom R Shiny application

Image 7 – Custom R Shiny application

As expected, you can change the continent value and the charts will re-render automatically:

Image 8 - Custom R Shiny application (2)

Image 8 – Custom R Shiny application (2)

The Shiny app created in this section is fairly simple, but we don’t need anything more complex. The purpose is to showcase how the Docker renv combination works together, after all.

In case you need a refresher on data visualization in R:

Creating an R renv Environment Snapshot

The final step in this section is to create an R environment snapshot. We’re doing this step now because renv will scan for the dependencies that are actually used in R scripts and won’t pick on any that are installed but unused.

Run the following command from the R console to create a snapshot:

renv::snapshot()

This is the output you will see:

Image 9 - Creating the environment snapshot

Image 9 – Creating the environment snapshot

Scroll down to the bottom and confirm the snapshot creation. As soon as you do, the folder structure in our isolated R environment will change:

Image 10 - Updated directory structure

Image 10 – Updated directory structure

The renv.lock file now also has a list of all dependencies and sub-dependencies used in our project:

Image 11 - Contents of a renv.lock file

Image 11 – Contents of a renv.lock file

And that’s it – you’ve done everything needed on the renv end. Now it’s time to start working with Docker.

Renv with Docker: How to Launch Your App in a Container

This article section will teach you how to write a Dockerfile suitable for using renv with Docker, and also how to create an image and container using a couple of shell commands.

Writing a Dockerfile for R renv

A Dockerfile is a simple set of instructions responsible for telling Docker how to build an image. We want to use a base image that already has R installed, and then copy our app and install all of the dependencies.

The syntax of this file will take you some time to get used to, so let’s go over all of the specific keywords used:

  • FROM: Tells Docker what is the base image on which your image will be based. For example, the rocker/shiny image already has R and Shiny installed, so it’s a good starting point.
  • RUN: Runs a shell command, such as the one for installing R packages, and creating directories, and files.
  • WORKDIR: Sets a working directory so you don’t have to specify a full path every time you want to copy a file.
  • COPY: Copies contents from a local machine to a Docker container. You have to specify the path to the local file first, followed by a path to the file on the container.
  • EXPOSE: Exposes a port of a Docker container so we can access it from a local machine.
  • CMD: Command used every time you launch the Docker container. We’ll use it to run our Shiny app.

To get started, create a new file and name it Dockerfile, without any extensions. Paste the following contents inside it:

# Base R Shiny image
FROM rocker/shiny

# Make a directory in the container
RUN mkdir /home/shiny-app

# Install Renv
RUN R -e "install.packages('renv', repos = c(CRAN = 'https://cloud.r-project.org'))"

# Copy Renv files and Shiny app
WORKDIR /home/shiny-app/

RUN mkdir -p renv
COPY app.R app.R
COPY renv.lock renv.lock
COPY .Rprofile  .Rprofile
COPY renv/activate.R renv/activate.R
COPY renv/settings.json renv/settings.json

# Restore the R environment
RUN R -e "renv::restore()"

# Expose the application port
EXPOSE 8180

# Run the R Shiny app
CMD Rscript /home/shiny-app/app.R

What we’re doing here is actually simple. We’re defining the base image, creating a folder to hold our Shiny app, installing renv, copying a bunch of files that contain our R Shiny application and environment info, running the renv::restore() function to restore an environment from the lock file, exposing a port on which the app will run, and finally, running the Shiny app.

Sounds like a lot, but go over it section by section – you’ll immediately understand what each part is responsible for.

Building a Custom Docker Image

We now have a Dockerfile saved, which means the only step left to do is to build the image and run the container.

Unfortunately, the rocker/shiny image isn’t yet supported on ARM platforms (such as M1/M2 Macs), so we’ll have to be a bit creative on this step.

But first, open up a new Terminal window and navigate to where your R project is saved.

Run this shell command if you’re using an x86_64 architecture:

docker build -t renv-docker-demo .

Similarly, run the following line if you’re using an ARM architecture:

docker build --platform linux/x86_64 -t renv-docker-demo .

We’re running this code on an M2 MacBook Air, so the second shell command does the trick for us:

Image 12 - Building the Docker image

Image 12 – Building the Docker image

The process of building the image will take some time, a couple of minutes at least. If you see that the R environment is restoring, it means you’re on the right track:

Image 13 - Building the Docker image (2)

Image 13 – Building the Docker image (2)

Once done, you’ll see the following output:

Image 14 - Image build success message

Image 14 – Image build success message

The only thing left to do is to create a container from this image.

Running the Docker Container

The following shell command will create a container based on the previously created image (renv-docker-demo) and will bind the local port 8180 to the one exposed by the Docker container:

docker run -p 8180:8180 renv-docker-demo

You’ll see the message that the app is running:

Image 15 - Running the container

Image 15 – Running the container

You can now open up your browser and open localhost on port 8180 – here’s what you’ll see:

Image 16 - Shiny app running in a Docker container

Image 16 – Shiny app running in a Docker container

This is the exact same app created in the previous section, but is now running in a Docker container and has all of the R dependencies locked because of renv.

As before, you can change the dropdown menu value, and the contents will re-render automatically:

Image 17 - Shiny app running in a Docker container (2)

Image 17 – Shiny app running in a Docker container (2)

And that’s how you can use renv with Docker! Let’s make a brief recap next.


Summing up renv with Docker

To summarize, you can use both renv and Docker separately, but combining them is what you really should strive for. Not only does it allow you to have full control of the underlying OS and system dependencies, but it also enables you to set R package versions in stone, so they don’t ever change.

It is the ultimate approach to making your projects reproducible, sharable, and running in an isolated environment – just what you want!

Are you combining renv with Docker in your R/R Shiny projects? Make sure to let us know in the comment section below.

No data to display in your Shiny app, or is data still loading? Make sure to deliver the best user experience with shiny.emptystate.

The post appeared first on appsilon.com/blog/.

To leave a comment for the author, please follow the link and comment on their blog: Tag: r - Appsilon | Enterprise R Shiny Dashboards.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)