Skip to contents

Overview

The pracpac package enables developers to easily incorporate R packages into Docker images. What follows is a reproducible demonstration of select pracpac use cases. Note that this vignette is by no means exhaustive, and the pattern of delivering an R package within a Docker image1 may prove useful in other scenarios as well.

Pipeline

The “pipeline” use case describes a scenario where a developer may want to use functions from a custom R package to perform processing on the input and/or output of a domain-specific tool. There are countless software packages that will not be implemented directly in R. Developers who want to leverage these tools in a reproducible context may choose to do so by using Docker. If the tool(s) in the workflow require(s) upstream or downstream processing that is best suited to R code, then the developer could write an R package and distribute everything together in Docker. Besides defining reproducible dependencies, Docker allows the developer to pass “instructions” for how the container should behave. These can include scripts to run when the container is launched.

To demonstrate this use case we use the hellow R package source code that ships with pracpac. We write a pipeline to use the isay() function from hellow to randomly select a flavor of “Hello”. The output is piped to a command-line tool called translate-shell2 that translates the text to another language specified by the user (by default French). When the container runs, the pipeline is executed and outputs the translated results.

Setting up the Docker template

We will first move the example hellow package to a temporary location:

library(pracpac)
library(fs)

## specify the temp directory
tmp <- tempdir()
## copy the example hellow package to the temp directory
dir_copy(path = system.file("hellow", package = "pracpac"), new_path = path(tmp, "example", "hellow"))

The new directory includes the R package source contents of hellow:

dir_tree(path(tmp, "example", "hellow"), recurse = TRUE)
├── DESCRIPTION
├── NAMESPACE
├── R
│   └── hello.R
└── man
    └── isay.Rd

We can use use_docker(..., use_case="pipeline") to create the template of files for building the Docker image:

use_docker(pkg_path = path(tmp, "example", "hellow"), use_case = "pipeline")
Using renv. Dockerfile will build from renv.lock in /tmp/RtmpsMexB6/example/hellow/docker.
Using template for the specified use case: pipeline
Writing dockerfile: /tmp/RtmpsMexB6/example/hellow/docker/Dockerfile
The directory will be created at /tmp/RtmpsMexB6/example/hellow/docker/assets 
Assets for the specified use case (pipeline) will be copied there.
The specified use case (pipeline) includes the following asset: run.sh
The specified use case (pipeline) includes the following asset: pre.R
The specified use case (pipeline) includes the following asset: post.R
Building package hellow version 0.1.0 in /tmp/RtmpsMexB6/example/hellow/hellow_0.1.0.tar.gz
docker build command:
docker build  --tag hellow:latest --tag hellow:0.1.0 /tmp/RtmpsMexB6/example/hellow/docker

The directory now contains the docker/ subdirectory, which has another subdirectory called assets/ for the templated pipeline scripts:

dir_tree(path(tmp, "example", "hellow"), recurse = TRUE)
├── DESCRIPTION
├── NAMESPACE
├── R
│   └── hello.R
├── docker
│   ├── Dockerfile
│   ├── assets
│   │   ├── post.R
│   │   ├── pre.R
│   │   └── run.sh
│   ├── hellow_0.1.0.tar.gz
│   └── renv.lock
└── man
    └── isay.Rd

The files need to be edited as follows:

Dockerfile

FROM rocker/r-ver:latest

## copy the renv.lock into the image
COPY renv.lock /renv.lock

## install renv
RUN Rscript -e 'install.packages(c("renv"))'

## set the renv path var to the renv lib
ENV RENV_PATHS_LIBRARY renv/library

## restore packages from renv.lock
RUN Rscript -e 'renv::restore(lockfile = "/renv.lock", repos = NULL)'

## copy in built R package
COPY hellow_0.1.0.tar.gz /hellow_0.1.0.tar.gz

## run script to install built R package from source
RUN Rscript -e 'install.packages("/hellow_0.1.0.tar.gz", type="source", repos=NULL)'

## COPY in the pre processing R script and run shell script
COPY assets/pre.R /pre.R
COPY assets/run.sh /run.sh

## add the translate-shell tool and dependencies
RUN apt-get update && apt-get install -y bsdmainutils translate-shell

## enter at run shell script
## allows for parameters passed to container at runtime
ENTRYPOINT ["bash", "/run.sh"]

run.sh

#!/bin/bash

## get tranlsation language from first argument
## default to :fr
TOLANG="${1:-:fr}"
HELLO=$(Rscript /pre.R)

trans "$HELLO" "$TOLANG"

pre.R

library(hellow)

isay()

Note that in this case the docker/assets/post.R template is not necessary, so we can delete it:

file_delete(path(tmp, "example", "hellow", "docker", "assets", "post.R"))

Building the image

With the template files created and edited as described above, we can build the image:

build_image(pkg_path = path(tmp, "example", "hellow"))

In this case, the image will be built with build_image() default behavior of tagging with the package name and version:

system("docker images")
hellow                            0.1.0        e1a9bc2ebbb5   15 seconds ago   959MB
hellow                            latest       e1a9bc2ebbb5   15 seconds ago   959MB

Running the container

Now we can run the container, either from a Docker client directly on the host or from within R:

system("docker run --rm hellow:latest")
You are peachy!
[1] "Hello"

[1] "Bonjour"

Translations of [1] "Hello"
[ English -> Français ]

[1] "Hello"
    [1] "Bonjour", [1] "Salut"
system("docker run --rm hellow:latest :es")
You are groundbreaking!
[1] "What's up?"

[1] "¿Qué pasa?"

Translations of [1] "What's up?"
[ English -> Español ]

[1] "What's up?"
    [1] "¿Qué pasa?", [1] "¿Qué hay de nuevo?"

Shiny

The “shiny” use case enables quick and intuitive templating of tools necessary to host a Shiny app in a Docker container. There are a number of ways that developers could ship a Shiny app inside an R package, and the example that follows demonstrates one strategy for doing so.

To demonstrate this use case we use the ocf R package source code that ships with pracpac. The ocf package wraps a custom Shiny app that we’ve titled “Old Colorful”. This app is intended to provide basic motivation for how a Shiny image could be templated, built, and used with pracpac. In short, the app code uses the bones of the “Old Faithful” Shiny app and simply adds a feature to add color to the eruptions histogram. The color palette is selected at random from palettes in the wesanderson package via a function called get_pal(). This function is embedded in a reactive context such that when the user clicks a button, the histogram plot title and bar colors dynamically update.

Setting up the Docker template

We will first move the example ocf package to a temporary location:

library(pracpac)
library(fs)

## specify the temp directory
tmp <- tempdir()
## copy the example ocf package to the temp directory
dir_copy(path = system.file("ocf", package = "pracpac"), new_path = path(tmp, "example", "ocf"))
dir_tree(path(tmp, "example", "ocf"), recurse = TRUE)
├── DESCRIPTION
├── NAMESPACE
├── R
│   └── pal.R
├── inst
│   └── app
│       └── app.R
└── man
    └── get_pal.Rd

We can use use_docker(..., use_case="shiny") to create the template of files for building the Docker image:

use_docker(pkg_path = path(tmp, "example", "ocf"), use_case = "shiny")
Using renv. Dockerfile will build from renv.lock in /tmp/RtmpsMexB6/example/ocf/docker.
Using template for the specified use case: shiny
Writing dockerfile: /tmp/RtmpsMexB6/example/ocf/docker/Dockerfile
The directory will be created at /tmp/RtmpsMexB6/example/ocf/docker/assets 
Assets for the specified use case (shiny) will be copied there.
The specified use case (shiny) includes the following asset: app.R
Building package ocf version 0.1.0 in /tmp/RtmpsMexB6/example/ocf/ocf_0.1.0.tar.gz
docker build command:
docker build  --tag ocf:latest --tag ocf:0.1.0 /tmp/RtmpsMexB6/example/ocf/docker

The directory now contains the docker/ subdirectory, which has another subdirectory called assets/ for the templated Shiny files:

dir_tree(path(tmp, "example", "ocf"), recurse = TRUE)
├── DESCRIPTION
├── NAMESPACE
├── R
│   └── pal.R
├── docker
│   ├── Dockerfile
│   ├── assets
│   │   └── app.R
│   ├── ocf_0.1.0.tar.gz
│   └── renv.lock
├── inst
│   └── app
│       └── app.R
└── man
    └── get_pal.Rd

One of the files needs to be edited as follows:

app.R

library(shiny)
library(ocf)

# Define UI for application that draws a histogram
ui <- fluidPage(

  # Application title
  titlePanel("Old Faithful Geyser Data (In Color!)"),

  # Sidebar with a slider input for number of bins
  sidebarLayout(
    sidebarPanel(
      sliderInput("bins",
                  "Number of bins:",
                  min = 1,
                  max = 50,
                  value = 30),
      actionButton(inputId = "color", label = "Add Color")
    ),

    # Show a plot of the generated distribution
    mainPanel(
      plotOutput("distPlot")
    )
  )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

  color_palette <- eventReactive(input$color, {
    get_pal()
  })

  output$distPlot <- renderPlot({
    # generate bins based on input$bins from ui.R
    x    <- faithful[, 2]
    bins <- seq(min(x), max(x), length.out = input$bins + 1)

    # draw the histogram with the specified number of bins
    hist(x,
         breaks = bins,
         col = color_palette()[[1]][1],
         border = color_palette()[[1]][5],
         main = paste0("Histogram in ", names(color_palette())))
  })
}

# Run the application
shinyApp(ui = ui, server = server)

Note that for this particular example, the templated Dockerfile requires no editing. In some cases, it may be necessary to edit the Dockerfile to modify behavior of the Shiny server (e.g., edit the shiny-server.conf file).

Building the image

With the template files created and edited as described above, we can build the image:

build_image(pkg_path = path(tmp, "example", "ocf"))

In this case, the image will be built with build_image() default behavior of tagging with the package name and version:

system("docker images")

Running the container

Now we can run the container, either from a Docker client directly on the host or from within R:

system("docker run --rm -it -d --user shiny -p 3838:3838 ocf:0.1.0")

In this case we run the container detached (-d) and use the port 3838 such we can navigate to localhost:3838 in a web browser and view the running app.