Deploy RShiny with the Rocker/Shiny Docker Image

docker r rshiny Apr 13, 2020

If you are deploying RShiny with docker you can roll your own image , or you can use the rocker Dockerhub image .

Which solution you go for will depend upon your own needs. Normally, I am using a lot of bioinformatics and/or data science packages and nothing really beats the conda ecosystem for that in my opinion. On the other hand having an image with RShiny already installed is really nice! I also quite like the way that the rocker/shiny image is configured. You drop your files into a directory and as long as you have the packages you need your shiny app will start up directly!

If you'd like to learn more about deploying RShiny please consider checking out my FREE Deploy RShiny on AWS Guide!

Deploy a simple shiny app using the rocker/shiny image

Here is our usual shiny app mostly stolen from the R shiny docs . ;-)

#!/usr/bin/env Rscript

# This example comes from the r-shiny examples github repo.
# https://github.com/rstudio/shiny-examples/blob/master/001-hello/app.R

library(shiny)

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

  # App title ----
  titlePanel("Hello Shiny!"),

  # Sidebar layout with input and output definitions ----
  sidebarLayout(

    # Sidebar panel for inputs ----
    sidebarPanel(

      # Input: Slider for the number of bins ----
      sliderInput(inputId = "bins",
                  label = "Number of bins:",
                  min = 1,
                  max = 50,
                  value = 30)

    ),

    # Main panel for displaying outputs ----
    mainPanel(

      # Output: Histogram ----
      plotOutput(outputId = "distPlot")

    )
  )
)


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

  # Histogram of the Old Faithful Geyser Data ----
  # with requested number of bins
  # This expression that generates a histogram is wrapped in a call
  # to renderPlot to indicate that:
  #
  # 1. It is "reactive" and therefore should be automatically
  #    re-executed when inputs (input$bins) change
  # 2. Its output type is a plot
  output$distPlot <- renderPlot({

    x    <- faithful$waiting
    bins <- seq(min(x), max(x), length.out = input$bins + 1)

    hist(x, breaks = bins, col = "#75AADB", border = "white",
         xlab = "Waiting time to next eruption (in mins)",
         main = "Histogram of waiting times")

    })

}

# If you want to automatically reload the app when your codebase changes - should be turned off in production
options(shiny.autoreload = TRUE)

options(shiny.host = '0.0.0.0')
options(shiny.port = 8080)

# Create Shiny app ---- 
shinyApp(ui = ui, server = server)

Make a directory called shiny-app and drop the above app.R in.

Serve your RShiny App

Now that you have an app to serve up you'll need to know two fundamental docker concepts. How to expose ports and how to mount volumes.

docker run --rm \
    -v "$(pwd)/shiny-app":/srv/shiny-server \
    -p 3838:3838

You should see a message about your RShiny app running. Open up a browser at localhost:3838 and you should see the following app.

Now, this is where the rocker image is pretty neat. Since we are dropping our app straight into the /srv/shiny-server it starts right up. To know why that is happening let's look at the magic in the Dockerfile.

Deep dive into the Rocker/Shiny image

FROM rocker/r-ver:3.6.3

RUN apt-get update && apt-get install -y \
    sudo \
    gdebi-core \
    pandoc \
    pandoc-citeproc \
    libcurl4-gnutls-dev \
    libcairo2-dev \
    libxt-dev \
    xtail \
    wget


# Download and install shiny server
RUN wget --no-verbose https://download3.rstudio.org/ubuntu-14.04/x86_64/VERSION -O "version.txt" && \
    VERSION=$(cat version.txt)  && \
    wget --no-verbose "https://download3.rstudio.org/ubuntu-14.04/x86_64/shiny-server-$VERSION-amd64.deb" -O ss-latest.deb && \
    gdebi -n ss-latest.deb && \
    rm -f version.txt ss-latest.deb && \
    . /etc/environment && \
    R -e "install.packages(c('shiny', 'rmarkdown'), repos='$MRAN')" && \
    cp -R /usr/local/lib/R/site-library/shiny/examples/* /srv/shiny-server/ && \
    chown shiny:shiny /var/lib/shiny-server

EXPOSE 3838

COPY shiny-server.sh /usr/bin/shiny-server.sh

CMD ["/usr/bin/shiny-server.sh"]

We can see here that the rocker/shiny image is using a base R Docker image, and then installing the shiny package. It's also copying a shiny-server.sh file from the build directory to the image. The shiny-server.sh is being used as the startup CMD. (Remember, RUN executes a command during build, and CMD executes a command when teh container is run with docker run. Let's investigate the shiny-server.sh startup file.

#!/bin/sh

# Make sure the directory for individual app logs exists
mkdir -p /var/log/shiny-server
chown shiny.shiny /var/log/shiny-server

if [ "$APPLICATION_LOGS_TO_STDOUT" != "false" ];
then
    # push the "real" application logs to stdout with xtail in detached mode
    exec xtail /var/log/shiny-server/ &
fi

# start shiny server
exec shiny-server 2>&1

What we see here is that the shiny-server.sh script is executing a command with an option to push logs to stdout or not. This is something that you will see fairly often in docker containers. When you are building you often want to see all of the log information, but for production systems you often want the logs pushed to files or logging infrastructure such as logstash or elasticsearch.

What is shiny-server doing?

I always like to dig into the containers themselves and see what's happening, especially when it comes to startup commands and web servers.

To dig around in a docker container supply the it flag (interative and tty) and run the bash command. This will drop you straight into a shell.

Exit out of the previous docker run command or don't supply a port. Otherwise this command will fail with an error about the port already being listened on.

docker run -it --rm \
    -p 3838:3838 \
    -v "$(pwd)/shiny-app":/srv/shiny-server \
    rocker/shiny bash

Now, if I run shiny-server --help it starts up a Node server and tells you about which configuration file it's looking at. Being the endlessly curious individual that I am I wanted to know more, so I went and took a look at the Github repo. As far as I can tell, and please don't hold me to this because I don't particularly speak javascript, but what it is doing is to read in your configuration file, and create a separate webserver per folder in the /srv/shinyapps, along with pretty routes that correspond to your folder names, which is how you get that nice drag, drop, and presto RShiny shinyness!

Create a custom Rocker/Shiny image

Now, if you want to install any additional system packages you will need to create a new image.

I also prefer to have my application in the docker image rather than have it mounted on the filesystem for production purposes, so we'll do that here too.

Your directory structure should look like this:

Dockerfile
shiny-app/
    app-R
# Dockerfile
FROM rocker/shiny:3.6.3

RUN apt-get update && apt-get install -y \
    things-i-want-to-install
    
COPY shiny-app/app.R /srv/shiny-app/app.R

Once you have your Dockerfile setup you just need to build your image.

docker build -t my-rshiny-app .

Except, you know, with a better name and more descriptive name that absolutely does not have thing or stuff in there.

Wrap Up

And that's it! Your docker image will inherit the startup command from the rocker/shiny image, so need to specify it here. Just add in your additional packages, copy your files, and prosper!

Looking for More?

If you're looking for more in-depth guides, ebooks, and project templates please consider checking out the Dabble of DevOps store and the RShiny Project Template.

Close

50% Complete

DevOps for Data Scientists Weekly Tutorials

Subscribe to the newsletter! You'll get a weekly tutorial on all the DevOps you need to know as a Data Scientist. Build Python Apps with Docker, Design and Deploy complex analyses with Apache Airflow, build computer vision platforms, and more.