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:
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"
)
)
)
)
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_
}
)
}
library(shinydashboard)
library(shiny)
library(tidyverse)
library(DT)
mydata_<-readRDS("mydata_.RDS")
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é
There are several pieces to this: either specify runApp(.., host=, port=)
or shift to using the built-in shiny-server in the parent image.
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.
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:
runApp(.)
fails, it stops. (Granted, this is governed by restart logic of shiny in the presence of clear errors in the code.)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.)