R Shiny Docker: How To Run Shiny Apps in a Docker Container
It doesn't matter if you've created the world's best R Shiny application if you can't share it with others. Reproducibility and portability are two major key points in software development, and they basically stand for the idea that the code running on your machine should be easily reproduced on another machine. That's where Docker comes in. In the context of R Shiny, Docker allows you to create and package an application that's ready to be shipped and recreated on another operating system. Think of it as an entire virtual machine running Shiny but without the unnecessary stuff. Today you'll learn why Docker is important for R Shiny applications, and how to get started using R Shiny Docker by coding an app from scratch and Dockerizing it. Let's dig in! <blockquote>Are your data science pipelines reproducible? <a href="https://appsilon.com/r-targets-reproducible-data-science-pipeline/" target="_blank" rel="noopener">Read our guide to clean and function-oriented pipelines with R targets</a>.</blockquote> <h3>Table of contents:</h3><ul><li><strong><a href="#why">Why R Shiny Docker - The Benefits</a></strong></li><li><strong><a href="#shiny-app">Writing the R Shiny Application from Scratch</a></strong></li><li><strong><a href="#dockerize">R Shiny Docker - How to Dockerize your Shiny App</a></strong></li><li><strong><a href="#summary">Summing up R Shiny Docker</a></strong></li></ul> <hr /> <h2 id="why">Why R Shiny Docker - The Benefits</h2> Docker is a platform for developing, shipping, and running applications in isolated environments, or <b>containers</b>, which is something you know if you've read our previous blog post on getting started with R and Docker. It can provide a lot of benefits for you in the context of R and R Shiny, such as: <ul><li><b>Consistency:</b> You want your code running consistently across different environments, such as development, testing, and production. Docker containers essentially encapsulate programming environments, so you can rest assured the code and dependencies will remain the same across all of them.</li><li><b>Reproducibility:</b> Docker images are highly flexible, meaning they allow you to specify the R version needed, R dependencies, and other OS-related dependencies. This ensures you (or anyone else) won't run into any issues when sharing your code. It's a write-once-run-anywhere system.</li><li><b>Portability:</b> The containers you create can be run on any system that supports Docker, such as servers, other laptops, home NAS solutions, and even Raspberry PI.</li><li><b>Scalability:</b> Docker makes it easy to scale applications, R Shiny included. You can create multiple containers with identical configurations and scale your app horizontally. This is a good way of future-proofing your infrastructure as your app's workload increases.</li></ul> There are many other, excellent reasons to use Docker, but we find these ones to be the most relevant in the context of R and R Shiny. As for the <a href="https://www.docker.com/products/docker-desktop/" target="_blank" rel="noopener">Docker installation</a>, we've covered it in a previous article, and it boils down to a couple of mouse clicks once you download the installation file from the web. <a href="https://docs.docker.com/desktop/install/linux-install/" target="_blank" rel="noopener">Linux</a> users might have to do a couple of extra steps, but it's all well-documented online. <h2 id="shiny-app">Writing the R Shiny Application from Scratch</h2> The focus of today's article isn't R Shiny, so the app we'll write will be relatively simple. We'll leverage the well-known <a href="https://appsilon.com/r-dplyr-gapminder/" target="_blank" rel="noopener">Gapminder dataset</a> to visualize the average life expectancy over time, and also the average GDP per capita. The user will be able to select the continent, and the app will automatically update when a change is detected. In case you need a refresher on data visualization in R: <ul><li><a href="https://appsilon.com/ggplot2-bar-charts/" target="_blank" rel="noopener">How to Make Stunning Bar Charts in R</a></li><li><a href="https://appsilon.com/ggplot2-line-charts/" target="_blank" rel="noopener">How to Make Stunning Line Charts in R</a></li></ul> If you have some experience with R Shiny, the dashboard code should read like plain English. If not, try tweaking a couple of things to see what happens. The only thing that should look new is a call to <code>options()</code> function at the top of <code>app.R</code>. We're using this function to explicitly define the host and port on which the app will run. You generally want to take maximum control over the deployment process, so that's why we highly recommend you do the same as we did Anyhow, here's the full source code for <code>app.R</code>: <pre><code class="language-r">library(shiny) library(dplyr) library(ggplot2) library(gapminder) <br># Specify the application port options(shiny.host = "0.0.0.0") options(shiny.port = 8180) <br> ui <- fluidPage( sidebarLayout( sidebarPanel( tags$h4("Gapminder Dashboard"), tags$hr(), selectInput(inputId = "inContinent", label = "Continent", choices = unique(gapminder$continent), selected = "Europe") ), mainPanel( plotOutput(outputId = "outChartLifeExp"), plotOutput(outputId = "outChartGDP") ) ) ) <br>server <- function(input, output, session) { # Filter data and store as reactive value data <- reactive({ gapminder %>% filter(continent == input$inContinent) %>% group_by(year) %>% summarise( AvgLifeExp = round(mean(lifeExp)), AvgGdpPercap = round(mean(gdpPercap), digits = 2) ) }) <br> # Common properties for charts chart_theme <- ggplot2::theme( plot.title = element_text(hjust = 0.5, size = 20, face = "bold"), axis.title.x = element_text(size = 15), axis.title.y = element_text(size = 15), axis.text.x = element_text(size = 12), axis.text.y = element_text(size = 12) ) <br> # Render Life Exp chart output$outChartLifeExp <- renderPlot({ ggplot(data(), aes(x = year, y = AvgLifeExp)) + geom_col(fill = "#0099f9") + geom_text(aes(label = AvgLifeExp), vjust = 2, size = 6, color = "#ffffff") + labs(title = paste("Average life expectancy in", input$inContinent)) + theme_classic() + chart_theme }) <br> # Render GDP chart output$outChartGDP <- renderPlot({ ggplot(data(), aes(x = year, y = AvgGdpPercap)) + geom_line(color = "#f96000", size = 2) + geom_point(color = "#f96000", size = 5) + geom_label( aes(label = AvgGdpPercap), nudge_x = 0.25, nudge_y = 0.25 ) + labs(title = paste("Average GDP per capita in", input$inContinent)) + theme_classic() + chart_theme }) } <br>shinyApp(ui = ui, server = server)</code></pre> This is what the app looks like when you run it: <img class="size-full wp-image-22141" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b0195f9b9335fb8c066b70_Image-1-Gapminder-R-Shiny-application.webp" alt="Image 1 - Gapminder R Shiny application" width="2490" height="1918" /> Image 1 - Gapminder R Shiny application As you would expect, the chart contents update when you change the dropdown menu value: <img class="size-full wp-image-22143" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b0195f93c80263c782d1c5_Image-2-Gapminder-R-Shiny-application-2.webp" alt="Image 2 - Gapminder R Shiny application (2)" width="2490" height="1918" /> Image 2 - Gapminder R Shiny application (2) To conclude, this R Shiny app is nothing fancy, but will serve us just fine for simple deployment purposes. Let's go over that next. <h2 id="dockerize">R Shiny Docker - How to Dockerize your Shiny App</h2> In this section, you'll see how to write a simple <code>Dockerfile</code> for an R Shiny application, how to create a Docker image from <code>Dockerfile</code>, and also how to create a container from the previously created image. <h3>Writing the Dockerfile</h3> Think of <code>Dockerfile</code> as an instruction file responsible for telling Docker how to build an image. In a nutshell, you pick a base image (typically Linux with R language installed), and then specify instructions for installing additional dependencies, copying project files, and running R scripts. It's a simple file when you get used to the syntax, but uses a couple of distinct keywords you have to learn: <ul><li><code>FROM</code>: A starting point for every <code>Dockerfile</code>. Used to tell Docker from which base image you'll build your own. A <code>rocker/shiny</code> image already has R and Shiny installed, so that's the one we'll use.</li><li><code>RUN</code>: Runs a shell command. Useful for creating files and folders, installing dependencies, and more.</li><li><code>COPY</code>: Copies contents from your local machine to the Docker container. You first specify the path to the local file, followed by the file on the container.</li><li><code>EXPOSE</code>: Exposes a port of a Docker container so you can access it from your local machine.</li><li><code>CMD</code>: Command that will be used every time you launch the container. We'll use it to launch the R Shiny app.</li></ul> You'll find the contents of our <code>Dockerfile</code> below. Create your file first (no extensions), and copy the following snippet: <pre><code class="language-bash"># Base R Shiny image FROM rocker/shiny <br># Make a directory in the container RUN mkdir /home/shiny-app <br># Install R dependencies RUN R -e "install.packages(c('dplyr', 'ggplot2', 'gapminder'))" <br># Copy the Shiny app code COPY app.R /home/shiny-app/app.R <br># Expose the application port EXPOSE 8180 <br># Run the R Shiny app CMD Rscript /home/shiny-app/app.R</code></pre> To summarize, we're using the latest <code>rocker/shiny</code> image since it already has R programming language and Shiny frameworks installed - less work for us to do. Then, we're creating a directory to store our app, install R package dependencies, copy the <code>app.R</code> file, exposing the port on which the Shiny app will run (the one hardcoded in <code>app.R</code>), and then finally using <code>Rscrit</code> to launch the app. The next step is to create our custom image from this <code>Dockerfile</code>. <h3>Creating the Image and Running the Container</h3> <b>Note:</b> At the time of writing this article (September 2023), the <code>rocker/shiny</code> base image isn't supported on Apple Silicon. If you're using an M-series Mac, you'll have to append the <code>--platform linux/x86_64</code> tag when building the image. To build an image from a <code>Dockerfile</code>, run the following command (make sure you navigate to the source code folder first): <pre><code class="language-bash">docker build -t shiny-docker-demo .</code></pre> This will create a new image called <code>shiny-docker-demo</code>. If you're using an Apple Silicon Mac like us, run this command instead: <pre><code class="language-bash">docker build --platform linux/x86_64 -t shiny-docker-demo .</code></pre> This is the Terminal output you will see: <img class="size-full wp-image-22145" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01961ec1e575073db0960_Image-3-Building-the-Docker-image.webp" alt="Image 3 - Building the Docker image" width="2490" height="1458" /> Image 3 - Building the Docker image Downloading the base image and R dependencies will take some time, and you'll see something similar once finished: <img class="size-full wp-image-22147" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01962fd64fc3742687a48_Image-4-Building-the-Docker-image-2.webp" alt="Image 4 - Building the Docker image (2)" width="2490" height="1458" /> Image 4 - Building the Docker image (2) Your Docker image is now created, which means the only thing left to do is to run it inside a container. To do so, run the following command (works on M1/M2 Macs): <pre><code class="language-bash">docker run -p 8180:8180 shiny-docker-demo</code></pre> Here's what you'll see in the Terminal: <img class="size-full wp-image-22149" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b019648cff4e061341ebab_Image-5-Running-the-Docker-container.webp" alt="Image 5 - Running the Docker container" width="2490" height="1458" /> Image 5 - Running the Docker container The R Shiny application is now running inside the Docker container on port 8180. You can access it from your local machine since we've exposed that port in the <code>Dockerfile</code>. Here's what the app looks like: <img class="size-full wp-image-22151" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01965233800ce2d368244_Image-6-Dockerized-R-Shiny-application.webp" alt="Image 6 - Dockerized R Shiny application" width="1956" height="1658" /> Image 6 - Dockerized R Shiny application As before, you can change the value in the dropdown menu and the charts will update automatically: <img class="size-full wp-image-22153" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01965233800ce2d36829e_Image-7-Dockerized-R-Shiny-application-2.webp" alt="Image 7 - Dockerized R Shiny application (2)" width="1960" height="1664" /> Image 7 - Dockerized R Shiny application (2) And that's it - you've successfully Dockerized an R Shiny application! <h3>Monitoring Dockerized R Shiny Apps</h3> The Docker Desktop application can do all sorts of powerful things. For example, you can click on the container that's running your R Shiny application to inspect the runtime logs and even the file system. You can see that the directory structure was created as per the <code>Dockerfile</code> instructions: <img class="size-full wp-image-22155" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b019678d7dfe95e40f7f59_Image-8-Docker-container-files-and-folders.webp" alt="Image 8 - Docker container files and folders" width="2540" height="1440" /> Image 8 - Docker container files and folders Further, you can monitor the resource usage under the <i>Stats</i> tab: <img class="size-full wp-image-22157" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b019676ee3169213c5473d_Image-9-Docker-container-resource-usage.webp" alt="Image 9 - Docker container resource usage" width="2540" height="1440" /> Image 9 - Docker container resource usage This tab might need some time to populate the chart values, but you get the point - there's a lot of tooling and monitoring built into the Docker Desktop application, so make sure to spend some time investigating. <hr /> <h2 id="summary">Summing up R Shiny Docker</h2> Congratulations - you now have a fully working R Shiny application running in a Docker container! Writing Dockerfiles is definitely something to get used to, but it will feel like a walk in the park after a couple of weeks. Today you've successfully Dockerized one Shiny application, but what if you want to run two or more Shiny apps at once in separate containers? Make sure to stay tuned to <a href="https://appsilon.com/blog" target="_blank" rel="noopener">Appsilon Blog</a> so you don't miss out on advanced deployment techniques. <i>What's your preferred way of sharing R Shiny applications? Do you use Docker or something else?</i> Let us know in the comment section below. <blockquote>Yes, R programming can significantly improve your business workflows - <a href="https://appsilon.com/r-programming-vs-excel-for-business-workflow/" target="_blank" rel="noopener">Here are 5 ways how</a>.</blockquote>