# install.packages('pak')
::pak('mjfrigaard/shinypak') pak
20 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.
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
20.1 What is Docker?
Three terms to know when working with Docker are Dockerfile
, image, and container. Containers and images work together but they serve different purposes in the Docker ecosystem.
Dockerfile
: The file containing instructions on how to build the Docker image.Docker Image: A snapshot of the Shiny application and its dependencies, built based on the
Dockerfile
.Docker Container: A runtime instance of the Docker image, isolated from the host system, ensuring consistent behavior across environments. When we 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.
20.1.1 How Docker works
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
Inside our app-packages, we can write a Dockerfile
(a text file containing a series of instructions) that defines how to build the Docker image. The image is a snapshot representing our Shiny app and its dependencies. Docker containers are instantiated with the build
command, which reads and executes the instructions inside the Dockerfile
and creates a runtime instance of the image.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Inconsolata'}}}%% flowchart TB subgraph App-Package subgraph Dockerfile A["Instructions"] --> B["Docker Image"] end subgraph Container B --> C["Docker Container"] C --> D["Image & dependencies are safely stored"] C --> E["Ensures consistency across environments"] end end F["Host System"] -->|Isolated from...| App-Package style A fill:#8dd38d,color:#000000; style B fill:#89D6FB,color:#000000,stroke:none,rx:10,ry:10; style C fill:#89D6FB,color:#000000,stroke:none,rx:10,ry:10; style F fill:#5c6192,color:#FFFFFF,stroke:none,rx:10,ry:10;
The image containing our applications’ dependencies is safely stored inside the container, which ensures consistency across environments. The container also isolates the image (and the application) from the host system.
20.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
The development process for your Shiny app-package doesn’t change until you’re ready to deploy your application.
20.3 Writing a Dockerfile
We’ll be using the sap
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
├── sap.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). Dockerfile
s are a blend of commands, numeric values, and character strings with the following general conventions:
Each line in the
Dockerfile
starts with an instruction. These aren’t case-sensitive, but it’s common practice to capitalize each command.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).
20.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
20.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"))'
20.3.3 Create location for app
RUN mkdir
creates a new/deploy
directory in the containerADD . /deploy
copies the files from the current directory (on the host machine) into the/deploy
directory inside the containerWORKDIR /deploy
sets the/deploy
directory as the working directory for any subsequent instructions.
RUN mkdir /deploy
ADD . /deploy
WORKDIR /deploy
20.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")'
20.3.5 Clean up
RUN rm -rf /deploy
cleans up and reduces the size of the container.
RUN rm -rf /deploy
20.3.6 Expose port
Make our Shiny app available on EXPOSE 8180
.
EXPOSE 8180
20.3.7 Launch app
When the container starts, set Shiny to listen on port 8180
, then launch the Shiny app from our sap
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(sap);sap::launch_app()"
20.4 Build the Docker 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(sap);sap::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:
20.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.1 (2024-06-14) -- "Race for Your Life"
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(sap);sap::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:
20.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 atmp/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 fromrocker
(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 theversion
of each package and whether toupgrade
(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")'
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.”
20.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$
^sap\.Rcheck$
^sap.*\.tar\.gz$
^sap.*\.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$
^sap\.Rcheck$
^sap.*\.tar\.gz$
^sap.*\.tgz$
^Dockerfile$
^\.dockerignore$
Recap
This has been a brief overview of using Docker to deploy your Shiny App-Package.
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
Read more about Docker in the official documentation.↩︎
Docker containers are similar to virtual machine environments (like VMware), but don’t use a significant portion of the hardware system’s resources.↩︎
If you follow the Docker ‘Walkthroughs’ in the Learning Center, you might see the
welcome-to-docker
container listed.↩︎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.↩︎I love the help files on https://linuxize.com/, and this tutorial is a nice introduction to the command-line.↩︎
We’ll need remotes to install our app-package locally (i.e., with
remotes::install_local(upgrade="never")
).↩︎.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).↩︎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’↩︎
This is the second post in a series (see the first post here).↩︎