Plumber + Shiny + Docker

Plumber + Shiny + Docker
Page content

Why?

  • Shiny will scale really well as the data is not duplicated in memory for each user
  • The data is housed in one location (plumber API)
  • The underlying data can be VERY large, which may not be suitable for a shiny app, but is ok for an R process to solve.
  • The API inner workings can be updated without redeploying the shiny application.

The docker-compose.yml

version: '3.7'
services:
    shinyapptest:
      build: ./shiny
      container_name: testshiny
      image: testshiny
      restart: unless-stopped
      networks:
            - backend
      ports:
          - 80:3838
    dockerizedapi:
      build: ./plumber
      container_name: dockerizedapi
      image: dockerizedapi
      restart: unless-stopped
      networks:
            - backend
networks:
  backend:
    external:
      name: backend
     
  • There are two services:
    • shinyapptest: Is the shiny app that calls the Plumber API. This app is available to the world in port 80.
    • dockerizedapi" The Plumber api that is only available to shinyapptest

The Dockerfiles

Plumber

FROM rocker/r-base
RUN apt-get update -qq && apt-get install -y \
  git-core \
  libssl-dev \
  libcurl4-gnutls-dev
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_cran(c("glue", "plumber", "magrittr"))'
COPY testShinyPlumber_*.tar.gz /app.tar.gz
RUN R -e 'remotes::install_local("/app.tar.gz")'
EXPOSE 3098
CMD R -e "testShinyPlumber::run_api()"

Shiny

FROM rocker/r-base
RUN apt-get update -qq && apt-get install -y \
  git-core \
  libssl-dev \
  libcurl4-gnutls-dev
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_cran(c("shiny", "plumber"))'
COPY testShinyPlumber_*.tar.gz /app.tar.gz
RUN R -e 'remotes::install_local("/app.tar.gz")'
EXPOSE 3838
CMD R -e "testShinyPlumber::run_app()"

The R Package testShinyPlumber

Notice that both docker cotainers call testShinyPlumber.

The Plumber API

This is saved as plumber.R inside the ./inst folder

library(plumber)

#* @apiTitle Plumber Example API

#* Echo back the input
#* @param mean
#* @param sd
#* @get /rnorm
function(mean = "0", sd = "sd") {
  rnorm(n = 1000, mean = as.numeric(mean), sd = as.numeric(sd))
}

The R Fuctions

run_api.R

#' Run Test API
#'
#' @return
#' @export
#'
#' @examples
#' \dontrun{
#' testShinyPlumber::run_api()
#' }
run_api <- function(){
  pr <- plumber::plumb(file = system.file("plumber.R", package="testShinyPlumber"))
  pr$run(host='0.0.0.0', port = 3098, swagger = FALSE)
}

get_draws.R

#' Get Draws
#'
#' @param mean
#' @param sd
#' @param host
#'
#' @return
#' @export
#'
#' @examples
#' \dontrun{
#' draws <- testShinyPlumber::get_draws(mean = 9, sd = 1)
#' }
get_draws <- function(mean, sd, host="localhost"){
  draws <- httr::GET(url = glue::glue('http://{host}:3098/rnorm'),
                     query = list(mean = mean,
                                  sd = sd)) %>%
    httr::content(., encode='json') %>%
    unlist()
  return(draws)
}

run_app.R

#' Run the Shiny Application
#'
#' @export
#' @importFrom shiny runApp
run_app <- function() {
  shiny::runApp(system.file("app", package = "testShinyPlumber"), port = 3838, host = "0.0.0.0")
}

Get Things Up and Running

docker-compose build
docker-compose up #-d if you want to run it as a service

Final Thoughts

  • I should comeback to this someday and add more details. For now, here is a public repo with all the code.