15  Docker


Docker


Docker containers ensure your Shiny applications are deployed with the necessary tools and resources (dependencies, libraries, etc.) without significantly interfering with the native operating system or hardware.

I’ve created the shinypak R package In an effort to make each section accessible and easy to follow:

Install shinypak using pak (or remotes):

# install.packages('pak')
pak::pak('mjfrigaard/shinypak')

Review the chapters in each section:

library(shinypak)
list_apps(regex = '^15')
## # A tibble: 1 × 2
##   branch    last_updated       
##   <chr>     <dttm>             
## 1 15_docker 2024-07-18 07:57:42

Launch an app:

launch(app = "15_docker")

Common problem when shipping apps

 

Docker Whale

With Docker, each Shiny application runs from an image in an isolated container, which ensures your app works consistently, regardless of where it’s deployed.1

15.1 What is Docker?

Two terms to know when working with Docker are container and image. Containers and images work together but they serve different purposes in the Docker ecosystem.

Image: A Docker image is a lightweight, standalone, and executable file that includes everything needed to run an application, including the code, runtime, system tools, libraries, and settings.

Containers: A Docker container is a runtime instance of a Docker image. When you run an image, Docker creates a container from that image. A Docker image can exist without a container, but all containers must be instantiated from an image.

15.1.1 How they work

Imagine your computer as a building. In this building, your operating system (OS) is like a big kitchen where everyone cooks. If someone (a software application) needs the oven at a specific temperature or requires a particular ingredient, this can interfere with what someone else (i.e., other applications) wants to cook. This situation is somewhat analogous to the competition for resources software applications can have while running on a particular machine.

Docker containers are like individual, self-contained mini-kitchens containing appliances, ingredients, and utensils. Each mini-kitchen can operate independently, regardless of what’s happening in the others. Docker images can contain different ‘recipes’ for software applications, and each application can have different requirements (dependencies, libraries, versions, etc.).2

Docker images are created through a build process, which uses a Dockerfile (a text file containing a series of instructions) to define how the image should be constructed. For a Shiny app, all of the application’s dependencies are stored inside the container, which ensures consistency across environments. The container also isolates the image (and the application) from the host system.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Ubuntu Mono', 'primaryColor': '#89D6FB', 'edgeLabelBackground':'#02577A'}}}%%

graph TD
  A[App-package Source Code] --> B(Dockerfile)
  B --> C[Docker Build Command]
  C --> D[Docker Image]
  D --> E[Docker Container]
  E --> F[Shiny App Runs in Container]
  F --> G[Browser Access to App]

15.2 Installing Docker

Follow the instructions found on the Docker website to install Docker Desktop. I’ll be demonstrating how to use Docker on a macOS (but it’s available on most operating systems).

After installation, Docker Desktop will initially show no active images/containers:3

Docker Desktop Containers (Mac)

The development process for your Shiny app-package doesn’t change until you’re ready to deploy your application.

15.3 The Dockerfile

We’ll be using the shinyAppPkgs example from the previous chapter as an example (the folder/file structure is below):

Launch app with the shinypak package:

launch('15_docker')
view app-package folder tree
├── DESCRIPTION
├── Dockerfile
├── NAMESPACE
├── R
   ├── data.R
   ├── display_type.R
   ├── ggp2_movies_app.R
   ├── mod_scatter_display.R
   ├── mod_var_input.R
   ├── launch_app.R
   ├── movies_server.R
   ├── movies_ui.R
   ├── scatter_plot.R
   └── testthat.R
├── README.md
├── app.R
├── data
   ├── movies.RData
   └── movies.rda
├── data-raw
   └── tidy_movies.R
├── inst
   ├── dev
   │   ├── R
   │   │   ├── devServer.R
   │   │   ├── devUI.R
   │   │   ├── dev_mod_scatter.R
   │   │   └── dev_mod_vars.R
   │   ├── app.R
   │   ├── imdb.png
   │   └── tidy_movies.fst
   ├── extdata
   │   └── movies.fst
   ├── prod
   │   └── app
   │       └── app.R
   └── www
       ├── bootstrap.png
       └── shiny.png
├── man
   ├── display_type.Rd
   ├── ggp2_movies_app.Rd
   ├── mod_scatter_display_server.Rd
   ├── mod_scatter_display_ui.Rd
   ├── mod_var_input_server.Rd
   ├── mod_var_input_ui.Rd
   ├── movies.Rd
   ├── launch_app.Rd
   ├── movies_server.Rd
   ├── movies_ui.Rd
   ├── scatter_plot.Rd
   └── test_logger.Rd
├── shinyAppPkgs.Rproj
├── tests
   ├── testthat
   │   ├── fixtures
   │   │   ├── make-tidy_ggp2_movies.R
   │   │   └── tidy_ggp2_movies.rds
   │   ├── helper.R
   │   ├── setup-shinytest2.R
   │   ├── test-app-feature-01.R
   │   ├── test-ggp2_app-feature-01.R
   │   ├── test-mod_scatter_display.R
   │   ├── test-mod_var_input.R
   │   ├── test-scatter_plot.R
   │   └── test-shinytest2.R
   └── testthat.R
└── vignettes
    └── test-specs.Rmd

16 directories, 54 files

When our application is ready to deploy, we’ll create a Dockerfile, which is a plain-text file (no extension). Dockerfiles are a blend of commands, numeric values, and character strings with the following general conventions:

  1. Each line in the Dockerfile starts with an instruction. These aren’t case-sensitive, but it’s common practice to capitalize each command.

  2. Comments or explanatory notes begin with #. These will be ignored by the Docker engine.

Below are two lines from the Dockerfile found in the excellent post titled, R Shiny Docker: How To Run Shiny Apps in a Docker Container. This is a great place to familiarize yourself with deploying a non-package Shiny application with Docker.4

# build image 
FROM rocker/shiny

# create location for app
RUN mkdir /home/shiny-app

As you can see, the Dockerfile combines instructions (FROM, RUN) with command-line arguments (mkdir). You don’t have to be a command-line expert to write a Dockerfile (but knowing a few can get you out of a jam 5).

15.3.1 Create image

Docker files start by building an image. In our case, we want an image configured to run R, which has been provided for us by the generous folks at the rocker project.

The Shiny rocker image is for Shiny apps:

FROM rocker/shiny

15.3.2 Install dependencies

Use RUN and R -e to install dependencies for our app in the container from CRAN.6

RUN R -e 'install.packages(c("rlang", "stringr", "shiny", "ggplot2", "remotes"))'

15.3.3 Create location for app

  1. RUN mkdir creates a new /deploy directory in the container

  2. ADD . /deploy copies the files from the current directory (on the host machine) into the /deploy directory inside the container

  3. WORKDIR /deploy sets the /deploy directory as the working directory for any subsequent instructions.

RUN mkdir /deploy
ADD . /deploy
WORKDIR /deploy

15.3.4 Install app-package

remotes::install_local() will the R package specified in the local directory (where our Shiny app-package lives), without upgrading dependencies.

RUN R -e 'remotes::install_local(upgrade="never")'

15.3.5 Clean up

RUN rm -rf /deploy cleans up and reduces the size of the container.

RUN rm -rf /deploy

15.3.6 Expose port

Make our Shiny app available on EXPOSE 8180.

EXPOSE 8180

15.3.7 Launch app

When the container starts, set Shiny to listen on port 8180, then launch the Shiny app from our shinyAppPkgs package. The port we exposed in the Dockerfile should match the shiny.port option.

CMD R -e "options('shiny.port'=8180,shiny.host='0.0.0.0');library(shinyAppPkgs);shinyAppPkgs::launch_app()"

15.4 Build image

The final Dockerfile we’ll use to launch our application is below:

FROM rocker/shiny
RUN R -e 'install.packages(c("rlang", "stringr", "shiny", "ggplot2", "remotes"))'
RUN mkdir /deploy
ADD . /deploy
WORKDIR /deploy
RUN R -e 'remotes::install_local(upgrade="never")'
RUN rm -rf /deploy
EXPOSE 8180
CMD R -e "options('shiny.port'=8180,shiny.host='0.0.0.0');library(shinyAppPkgs);shinyAppPkgs::launch_app()"

To build the Docker image and create a new container, run the docker build command in the Terminal with a tag (-t), a name for the image (movies-app-docker-demo), and the location (.):

docker build -t movies-app-docker-demo .

As the docker image builds you’ll see the output in the Terminal:

After the image is built, we’ll see a new image listed in Docker desktop:

15.4.1 Running the container

After building the image, we can run the new container using docker run

docker run -p 8180:8180 movies-app-docker-demo

In the Terminal, we’ll see an R session initialize, and the R function calls from the last line of our Dockerfile:

R version 4.4.0 (2024-04-24) -- "Puppy Cup"
Copyright (C) 2024 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu

R is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.

  Natural language support but running in an English locale

R is a collaborative project with many contributors.
Type 'contributors()' for more information and
'citation()' on how to cite R or R packages in publications.

Type 'demo()' for some demos, 'help()' for on-line help, or
'help.start()' for an HTML browser interface to help.
Type 'q()' to quit R.

> options('shiny.port'=8180,shiny.host='0.0.0.0');library(shinyAppPkgs);shinyAppPkgs::launch_app()
Loading required package: shiny

Listening on http://0.0.0.0:8180

Copy the hyperlink above and place it in the browser to view the application:

New Docker containers are named using a random combination of adjectives and famous scientists’ surnames (unless the --name flag is added).

You can change the name of a Docker image by stopping the container and passing docker rename <old_name> <new_name> to the Terminal:

docker rename unruffled_bhabha launch_app

Each Docker container is created from the image (which is specified in the Dockerfile). The image serves as a blueprint for the containers, and we could create multiple containers from the same image:

Running and stopping Docker containers

15.5 Docker & golem

The golem package has multiple functions for building Docker files and images. There are a few notable points to make about some of the Docker images created with golem:

  • golem’s Docker functions can produce multiple Docker files (golem::add_dockerfile_with_renv() creates a tmp/deploy folder and adds the following files)

    View deploy/ folder
    deploy/
      ├── Dockerfile
      ├── Dockerfile_base
      ├── README
      ├── gap_0.0.0.9000.tar.gz
      └── renv.lock.prod
  • golem’s Docker files typically use the R build from rocker (and include a version):

    View R version build
    FROM rocker/verse:4.3.2
  • golem Docker files might also include additional commands for installing/updating command-line (linux) tools for downloading/exchanging data:

    View apt-get commands
    RUN apt-get update && apt-get install -y  libcurl4-openssl-dev libicu-dev libssl-dev libxml2-dev make pandoc zlib1g-dev
  • They also usually the remotes package to specify the version of each package and whether to upgrade (or not):

    View remotes::install_version()
    RUN R -e 'install.packages("remotes")'
    RUN Rscript -e 'remotes::install_version("rlang",upgrade="never",version="1.1.2")'
    RUN Rscript -e 'remotes::install_version("stringr",upgrade="never",version="1.5.1")'
    RUN Rscript -e 'remotes::install_version("shiny",upgrade="never",version ="1.8.0")'
    RUN Rscript -e 'remotes::install_version("ggplot2",upgrade="never",version="3.4.4")'

Both R -e and RUN Rscript -e will execute R expressions from the command line.

  • R -e is straightforward and typically used to evaluate a single expression (i.e., install.packages())

  • RUN Rscript -e is more commonly used for running scripts or more specialized commands:

pkgs <- c('glue', 'cli')
install.packages(pkgs)

There are additional differences, but these are important if you want to include additional requirements or control the version of R (or a package dependency). golem’s Docker images are more (you guessed it) opinionated, but every time I’ve used one it works right “out of the box.”

15.6 Docker in app-packages

You can include a Dockerfile in an app-package by adding it to the .Rbuildignore file.7

^.*\.Rproj$
^\.Rproj\.user$
^shinyAppPkgs\.Rcheck$
^shinyAppPkgs.*\.tar\.gz$
^shinyAppPkgs.*\.tgz$
^Dockerfile$

This will ensure it won’t interfere with your app-package builds. Docker also has it’s own ignore file (.dockerignore), which can include similar contents to the .gitignore:

.RData
.Rhistory
.git
.gitignore
manifest.json
rsconnect/
.Rproj.user

Note that if you include a .dockerignore file, you should also include this pattern in the .Rbuildignore:

^.*\.Rproj$
^\.Rproj\.user$
^shinyAppPkgs\.Rcheck$
^shinyAppPkgs.*\.tar\.gz$
^shinyAppPkgs.*\.tgz$
^Dockerfile$
^\.dockerignore$

Recap

This has been a brief overview of using Docker to deploy your Shiny App-Package.

Recap:

Docker files are used to build images, which defines an environment for the Shiny application to run. The container is the actual running instance of that environment.

  • Docker images are immutable, meaning they do not change. Once an image is created, it remains unchanged inside a container.

  • Docker containers can be started, stopped, moved, and deleted, but each time a container is run, it’s created from an image.

Docker is covered again in the golem chapter and on the Shiny frameworks supplemental website. If you’d like to learn more, Appsilon has a great introduction to Docker (mentioned above).8 I also found the Dockerizing shiny applications post helpful. 9

Please open an issue on GitHub


  1. Read more about Docker in the official documentation.↩︎

  2. Docker containers are similar to virtual machine environments (like VMware), but don’t use a significant portion of the hardware system’s resources.↩︎

  3. If you follow the Docker ‘Walkthroughs’ in the Learning Center, you might see the welcome-to-docker container listed.↩︎

  4. I’ve altered the comments of the original Dockerfile, so be sure to read the entire blog post before copying + pasting this into your workflow.↩︎

  5. I love the help files on https://linuxize.com/, and this tutorial is a nice introduction to the command-line.↩︎

  6. We’ll need remotes to install our app-package locally (i.e., with remotes::install_local(upgrade="never")).↩︎

  7. .Rbuildignore includes files that we need to have in our app-package, but don’t conform to the standard R package structure (and shouldn’t be included when building our app-package from the source files).↩︎

  8. Appsilon has a few posts on Docker: ‘R Shiny Docker: How To Run Shiny Apps in a Docker Container’ and ‘Renv with Docker: How to Dockerize a Shiny Application with an Reproducible Environment↩︎

  9. This is the second post in a series (see the first post here).↩︎