Search code examples
rdockershinydeploymentshinydashboard

How to deploy my shiny application (with multiple files) via Docker


I want to deploy a shiny application composed of files ui , server and global via Docker. All the files are in the folder deploy_test I simulated this dataset

set.seed(123)
dir.create("deploy_test")
setwd("deploy_test")
mydata_<-data.frame(
  gender=c(rep("Male",50),rep("Female",25)),
  height=c(rnorm(50,1.70,0.05),rnorm(25,1.65,1))
)
saveRDS(mydata_,file = "mydata_.RDS")

Here are the contents of my files:

1. UI

source("global.R")

dashboardPage(
  dashboardHeader(title = "Test of app deployment"),
  dashboardSidebar(
    
    selectInput("gender","Gender",as.character(unique(mydata_$gender)))
  ),
  dashboardBody(
    
    fluidRow(
      column(6,plotOutput(
        "plot1"
      )),
      column(6,plotOutput(
        "plot2"
      ))
    ),
    fluidRow(
      dataTableOutput(
        "table"
      )
    )
  )
)

2. SERVER

source("global.R")

function(input, output, session){
  
  output$plot1<-renderPlot(
    {
      data_<-mydata_%>%filter(
        gender==input$gender
      )
      boxplot(data_$height)
    }
  )
  
  
  output$plot2<-renderPlot(
    {
      data_<-mydata_%>%filter(
        gender==input$gender
      )
      hist(data_$height)
    }
  )
  
  
  output$table<-renderDataTable(
    {
      data_<-mydata_%>%filter(
        gender==input$gender
      )
      data_
    }
  )
  
  
}

3. GLOBAL

library(shinydashboard)
library(shiny)
library(tidyverse)
library(DT)
mydata_<-readRDS("mydata_.RDS")

4. DOCKERFILE

Dockerfile is located in the same folder as the Shiny's:

# Base image
FROM rocker/shiny

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


# Install dependencies
RUN R -e "install.packages(c('tidyverse','shiny','shinydashboard','DT'))"

COPY . /home/shiny-app/

EXPOSE 8180

CMD ["R", "-e", "shiny::runApp('/home/shiny-app')"]

I built my container without any problem:

docker build -t deploy_test .

When I run it:

docker run -p  8180:8180 deploy_test

It generate the links: Listening on http://xxx.x.x.x:xxxx

But nothing appears when I access the link: I got: La connexion a échoué


Solution

  • There are several pieces to this: either specify runApp(.., host=, port=) or shift to using the built-in shiny-server in the parent image.

    Fix runApp

    First is that you expose port 8180 but the default of runApp may be to randomly assign a port. From ?runApp:

        port: The TCP port that the application should listen on. If the
              ‘port’ is not specified, and the ‘shiny.port’ option is set
              (with ‘options(shiny.port = XX)’), then that port will be
              used. Otherwise, use a random port between 3000:8000,
              excluding ports that are blocked by Google Chrome for being
              considered unsafe: 3659, 4045, 5060, 5061, 6000, 6566,
              6665:6669 and 6697. Up to twenty random ports will be tried.
    

    My guess is that it does not randomly choose 8180, at least not reliably enough for you to count on that.

    The second problem is that network port-forwarding using docker's -p forwards to the container host, but not to the container's localhost (127.0.0.1). So we also should assign a host to your call to runApp. The magic '0.0.0.0' in TCP/IP networking means "all applicable network interfaces", which will include those that you don't know about before hand (i.e., the default routing network interface within the docker container). Thus,

    CMD ["R", "-e", "shiny::runApp('/home/shiny-app',host='0.0.0.0',port=8180)"]
    

    When I do that, I'm able to run the container and connect to http://localhost:8180 and that shiny app works. (Granted, I modified the shiny code a little since I don't have your data, but that's tangential.)

    FYI, if you base your image on FROM rocker/shiny-verse instead of FROM rocker/shiny, you don't need to install.packages('tidyverse'), which can be a large savings. Also, with both rocker/shiny and rocker/shiny-verse, you don't need to install.packages('shiny') since it is already included. Two packages saved.

    Use the built-in shiny-server

    The recommended way to use rocker/shiny-verse is to put your app in /srv/shiny-server/appnamegoeshere, and use the already-functional shiny-server baked in to the docker image.

    Two benefits, one consequence:

    • Benefit #1: you can deploy and serve multiple apps in one docker image;
    • Benefit #2: if/when the shiny fails or exits, the built-in shiny-server will automatically restart it; when runApp(.) fails, it stops. (Granted, this is governed by restart logic of shiny in the presence of clear errors in the code.)
    • Consequence: your local browser must include the app name in the URL, as in http://localhost:8180/appnamegoeshere. The http://localhost:8180 page is a mostly-static landing page to say that shiny-server is working, and it does not by default list all of the apps that are being served by the server.

    This means that your Dockerfile could instead be this:

    # Base image
    FROM rocker/shiny-verse
    
    # Install dependencies (tidyverse and shiny are already included)
    RUN R -e "install.packages(c('shinydashboard', 'DT'))"
    
    # Make a directory in the container
    RUN mkdir /srv/shiny-server/myapp
    COPY . /srv/shiny-server/myapp
    

    That's it, nothing more required to get everything you need since CMD is already defined in the parent image. Because shiny-server defaults to port 3838, your run command is now

    docker run -p 3838:3838 deploy_test
    

    and your local browser uses http://localhost:3838/myapp for browsing.

    (FYI, the order of RUN and other commands in a Dockerfile can be influential. If, for instance, you change anything before the install.packages(.), then when you re-build the image it will have to reinstall those packages. Since we're no longer needing to (re)install "tidyverse" this should be rather minor, but if you stick with rocker/shiny and you have to install.packages("tidyverse"), then this can be substantial savings. By putting the RUN and COPY commands for this app after install.packages(..), then if we rename the app and/or add more docker commands later, then that install.packages step is cached/preserved and does not need to be rerun.)