Renv with Docker: How to Dockerize a Shiny Application with an Reproducible Environment
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?
- Creating an R Shiny Application with renv
- Renv with Docker: How to Launch Your App in a Container
- Summing up renv with Docker
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:
As soon as the new project is initialized, you’ll see the following output in the R console:
And if you were to navigate to the project folder, you’d see a bunch of R environment files:
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:
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:
Simply type “Y” and the packages will be installed in your isolated environment:
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:
As expected, you can change the continent value and the charts will re-render automatically:
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:
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:
The renv.lock
file now also has a list of all dependencies and sub-dependencies used in our project:
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, therocker/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:
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:
Once done, you’ll see the following output:
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:
You can now open up your browser and open localhost on port 8180 – here’s what you’ll see:
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:
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/.
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.