using xlsx package to save workbook within shiny application: Running Shiny server in docker container

shiny
docker
xlsx

#1

...Hi,

This is my first post on here, I've tried to follow the guidelines, but please let me know if there's any more info I can provide or any posting protocol faux-pas in any of the below.

I've built a shiny app that relies on the xlsx package in R to create an excel workbook that the user can then download. The app runs fine locally, but in attempting to deploy it in a docker container on a linux VM I'm having a lot of trouble.

A basic example of the app is as follows

server.R

   library(xlsx)

savefile <- function(filename){
  a <- data.frame(a = c(2,4,7), b = c("blue", "red", "yellow"))
  b <- data.frame(a = c(5,8,9), b = c("dog", "cat", "monkey"))
  # Create a results file
  wb <- createWorkbook(type = "xlsx")
  # Create excel output Sheets
  sheet_data1 <- createSheet(wb, sheetName = "Data")
  sheet_data2 <- createSheet(wb, sheetName = "Data2" )
  #add data to the first sheet
  addDataFrame(a, sheet_data1, startRow=1, startColumn=1)
  #add data to the second sheet
  addDataFrame(b, sheet_data2, startRow=1, startColumn=1)
  #save the workbook
  saveWorkbook(wb, paste0("temp/",filename,".xlsx"))
}

function(input, output, session) {
  session$onSessionEnded(function(){
    file.remove("temp/test file.xlsx")
  })

  output$downloadData <- downloadHandler(filename = function(){paste0(input$downloadName,".xlsx")},
                                         content = function(file){
                                           file.copy("temp/test file.xlsx", file)
                                           })
  
  observeEvent(input$sensoryCall, {
      savefile("test file")
    })
}

and ui.R

    
library(shinydashboard)

dashboardPage(skin="purple",
  
    dashboardHeader(title = "Analysis"),
    
    dashboardSidebar(
      sidebarMenu(
      menuItem("Run Analysis", tabName = "analyse", icon = icon("flash")),
      menuItem("Export Results", tabName = "export", icon = icon("download"))
      )
    ),
    
  dashboardBody(
    tabItems(
      tabItem(tabName = "analyse",
              h2("Run Sensory Analysis"),
              fluidRow(
                box(actionButton("sensoryCall", "Run Analysis", class='btn-primary'))
              )
      ),
      tabItem(tabName = "export",
            h2("Export Results to Excel"),
              fluidRow(
                box(textInput("downloadName", "Specify desired name of results file"),
                    downloadButton("downloadData", "Download Results"))
              )
      )
    )
  )
)

It took a while to get the rJava dependency sorted with the dockerfile set-up, I'd describe myself as a relative noob when it comes to linux and docker, but I've got it to a point where the R package checking seems to work for both rJava and xlsx with the following dockerfile set up, although there are a few warnings that come up in the build log, so this could be the route of the issue, but I'm at a stage where the messages and what I could do to fix them are well above my level of understanding.

FROM rocker/shiny-verse:latest

#Install wget and gnupg
RUN apt-get update && apt-get install -my wget gnupg
RUN apt-get update && apt-get install -y  libbz2-dev  liblzma-dev libicu-dev

#Install oracle java 8
RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections \
    && echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee /etc/apt/sources.list.d/webupd8team-java.list \
    && echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee -a /etc/apt/sources.list.d/webupd8team-java.list \
    && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys EEA14886 \
    && apt-get update \
    && apt-get install oracle-java8-installer -y

# clean local repository
RUN apt-get clean

# set up JAVA_HOME
ENV JAVA_HOME /usr/lib/jvm/java-8-oracle

RUN R CMD javareconf

#Install R packages
RUN R -e "install.packages(c('shiny', 'shinydashboard', 'rJava', 'xlsx'))"

When I click on the run analysis button in the containerised app, I get the following error message " Error: java.io.FileNotFoundException: test file.xlsx (Permission denied)" so I guess what's happening is that the app does not have permission to save the results file, but is there an easy way to either grant these permissions, or is there a better workaround that I could look to apply?

Thanks in advance for any suggestions offered


#2

Very interesting! You can check the permissions on the directory where you are saving the file from the command line with:

ls -lha

There are many articles on the internet explaining how to set and alter linux file permissions (things like chmod -R 755, chown -R ...). Usually, the /tmp folder has open file permissions, so that is one place you can save the file to check whether it is a permission problem. Wrappers like fs::file_temp and base::tempfile make this pretty straightforward, or you can craft your own (i.e. /tmp/myfile.txt).

Otherwise, your debugging should focus on the permissions and ownership for the file where you are trying to write the file. You can drop into a shell of a running docker container for some of this exploration by using

docker exec -it mycontainer bash`

#3

Thanks Cole,

I've tried saving to the tmp folder and that seems to be working ok. Having a look at some other articles on the internet, I found the suggestion to use

sudo chown -R shiny:shiny temp

in the app folder to change the permissions for the subdirectory, and running ls -lha in the app folder shows that on the VM this has updated the permissions, however when I spin up the container and access the app, I still get the issue when trying to save to the temp folder in the app. I'm guessing it would be unwise to just use the /tmp folder for this purpose? If I did, would the onSessionEnd file.remove work if I pointed it at the right folder?

Dropping in to the container as you mentioned shows all the permissions as 4 digit numbers rather than any of the user names that I get when working on the VM itself, I'm guessing there's a good reason for this, but I'm not sure what to do with this information

UPDATE
After a little more trial and error, I can use the sudo chown line inside the docker container and it seems to work, in that I can then save the files down to the temp folder inside the app, however, if I try and add the same line with RUN and the full path in my dockerfile it claims that the directory does not exist. Very curious


#4

Are you using VOLUMEs by any chance? The ephemeral nature of your folder sounds like a very docker-volume-esque issue. Docker has a bit more tricks than your standard linux filesystem, and that seems to be what you are running into. You want to be sure that file permissions are set appropriately when the container starts up, but volumes are not created/available during the image build process (I.e. in your RUN commands). Instead, they are added at run-time. Also, docker containers have different UIDs than the host system. Lots of quirks :slight_smile:

In any case, I am glad to hear that you have found a solution! The /tmp/whatever folder should work fine, even with the onSessionEnd file.remove or unlink commands to clean up the files. (Note that there are also a handful of ways to do file clean-up. I would recommend testing a few and ensuring that you are getting the behavior you expect!


#5

I haven't tested your example but I have an dockerised shiny application that allows a user to download an xlsx workbook with a button click.

Relevant server.R code where check_data is a list of data frames

  # Download output of check's ----
  output$downloadSolution <- downloadHandler(
    filename = function() { 
      paste("mismatches.xlsx") 
    },
    content = function(file) {
      
      write.xlsx(check_data, file = file, asTable = TRUE, borders = "columns", firstRow = TRUE, colWidths = "auto")
    }
  )

relevant ui.R

 downloadButton('downloadSolution', 'Download Mismatches File', style="color: #fff; background-color: #27ae60; border-color: #00a9e0"))

Hope this helps!


#6

I am indeed using volumes, sorry, should have included that info, I've basically got the standard docker-compose file setup described in the shiny rocker documentation. Thanks for sharing the links to the different clean up options, I'll give them a go and see what I get


#7

Very cool! Definitely let us know how it goes!

Some other terminology to be aware of. entrypoint / cmd can be used to change the container during startup, but is very dependent on what container you are using. Also, some/much of the file ownership of volumes / etc. is inherited from the OS that you mount in. I.e. if you chmod +x a file and then mount it into the container, it will be executable in the container.


#8

Thanks Tony, I think the slight complication on my end is that the function that runs and creates the excel output that I have is quite large (reduced to a basic version above for ease of reading) and bundled up with a load of calculations based on some input dataset provided by the user. The function was designed for working outside of a shiny application and I'm attempting to hook it up within the app with limited restructuring, but changing the function to return a list and moving the excel file creation functionality in to the downloadHandler may be a better long term solution and save some of the headaches around storing and cleaning up the temporary files in the long run, will see how I get on with the current set-up, but will keep this idea in mind :slight_smile: