Dockerizing Shiny Applications
Tags: shiny, rstats
Published: 2021-05-10 19:55:52

<p><em>By: <a href='https://hosting.analythium.io/author/peter/'>Peter Solymos</a></em></p><p>All the general advantages of containerized applications apply to Shiny apps. Docker provides isolation to applications. Images are immutable: once build it cannot be changes, and if the app is working, it will work the same in the future. Another important consideration is scaling. Shiny apps are single threaded, but running multiple instances of the same image can serve many users at the same time. Let's dive into the details of how to achieve this.</p><p>This post is based on the <a href="https://gitlab.com/analythium/shinyproxy-hello/" rel="noopener noreferrer">analythium/shinyproxy-hello</a> GitLab project. Readt about the <a href="https://hosting.analythium.io/the-anatomy-of-a-shiny-application/">Shiny example</a> and <a href="https://hosting.analythium.io/docker-basics-for-data-apps/">basic docker concepts</a> before continuing. The project contains the demo Shiny app in the <code>app</code> folder:</p>
```bash
.
├── app
│   ├── global.R
│   ├── server.R
│   └── ui.R
├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile
├── LICENSE
├── README.md
└── shinyproxy-hello.Rproj
```
<h2 id="working-with-an-existing-image">Working with an existing image</h2><p>First you'll learn how to work with an existing image.</p><h3 id="pull-image">Pull image</h3><p>You can pull the image made from the GitLab project's <a href="https://gitlab.com/analythium/shinyproxy-hello/container_registry">container registry</a> using the <code>docker pull</code> CLI command (you need to have the <a href="https://docs.docker.com/desktop/">Docker Desktop</a> running on our local machine):</p><pre><code class="language-bash">docker pull registry.gitlab.com/analythium/shinyproxy-hello/hello
</code></pre><h3 id="image-tag">Image tag</h3><p>The image is tagged as <code>REGISTRY/USER/PROJECT/IMAGE:TAG</code>. In this case the <code>TAG</code> is <code>latest</code> which is the default tag when not specified otherwise. (When the <code>REGISTRY</code> os not provided, Docker uses the Docker Hub as the default and you can follow the <code>USER/IMAGE:TAG</code> pattern).</p><h3 id="authentication">Authentication</h3><p>You don't need to authenticate for public images (like this one), but in case you are trying to pull a private image from a private GitLab project, you need to log into the GitLab Container registry as:</p><pre><code class="language-bash">docker login registry.gitlab.com
</code></pre><p>This command will ask for our credentials interactively. If you want, you can provide your username and password. But it is usually recommended to use a personal access token (PAT) instead of your password because PAT can have more restricted scopes, i.e. only used to (read) access the container registry which is a lot more secure.</p><pre><code class="language-bash">cat ~/my_password.txt | docker login --username USER --password-stdin
</code></pre><p>where <code>~/my_password.txt</code> is a file with the PAT in it, <code>USER</code> is the GitLab username.</p><h3 id="run-container">Run container</h3><p>After pulling the image, you can use <code>docker run</code> to run a command in a new container based on the image. Use the <code>-p 4000:3838</code> argument and the image tag:</p><pre><code class="language-bash">docker run -p 4000:3838 registry.gitlab.com/analythium/shinyproxy-hello/hello
</code></pre><p>The <code>-p</code> is a shorthand for <code>--publish</code>, that instructs Docker to publish a container’s port to the host port. In our example, 3838 is the container's port which is mapped to the port 4000 of the host machine. As a result, you can visit <code>127.0.0.1:4000</code> where you'll find the Shiny app. Hit Ctrl+C to stop the container.</p><p>Read all about the <code>docker run</code> command <a href="https://docs.docker.com/engine/reference/commandline/run/">here</a>. You'll learn about the 3838 port in a bit.</p><h3 id="shiny-host-and-port">Shiny host and port</h3><p>When we discussed <a href="https://hosting.analythium.io/run-shiny-apps-locally/">local hosting of Shiny apps</a> and <code>runApp</code>, we did not review all the possible arguments to this R function. Besides the app location (app object, list, file, or directory) there are two other important arguments:</p><ul><li><code>host</code>: this defines the IP address (defaults to 'localhost': 127.0.0.1),</li><li><code>port</code>: TCP port that the application should listen on; a random port when no value provided.</li></ul><p>When you run the shiny app locally, you see a message <code>Listening on http://127.0.0.1:7800</code> or similar, which is the protocol (HTTP), the host address, and the port number. The Shiny app is running in a web server that listens to client requests, and provides a response.</p><h2 id="build-a-new-image">Build a new image</h2><p>So far you saw how to use the <a href="https://hosting.analythium.io/docker-basics-for-data-apps/">basic docker commands</a> to pull and run images. Now you'll build a Docker image by recreating the simple Shiny app that we worked with before.</p><h3 id="create-the-dockerfile">Create the Dockerfile</h3><p>Create a file named <code>Dockerfile</code> (<code>touch Dockerfile</code>) then open the file and copy the following text into the file and save:</p>
```dockerfile
FROM rocker/r-base:latest
LABEL maintainer="USER &lt;user@example.com&gt;"
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    sudo \
    libcurl4-gnutls-dev \
    libcairo2-dev \
    libxt-dev \
    libssl-dev \
    libssh2-1-dev \
    &amp;&amp; rm -rf /var/lib/apt/lists/*
RUN install.r shiny
RUN echo "local(options(shiny.port = 3838, shiny.host = '0.0.0.0'))" &gt; /usr/lib/R/etc/Rprofile.site
RUN addgroup --system app \
    &amp;&amp; adduser --system --ingroup app app
WORKDIR /home/app
COPY app .
RUN chown app:app -R /home/app
USER app
EXPOSE 3838
CMD ["R", "-e", "shiny::runApp('/home/app')"]
```
<p>You can use the <code>docker build</code> command to build the image from the <code>Dockerfile</code>:</p><pre><code class="language-bash">docker build -t registry.gitlab.com/analythium/shinyproxy-hello/hello .
</code></pre><p>The <code>-t</code> argument is the tag, the <code>.</code> at the end refers to the <em>context</em> of the build, which is our current directory (i.e. where the <code>Dockerfile</code> resides) with a set of files based on which the image is built. So this is where the <code>app</code> folder and the R scripts need to be placed.</p><p>Once the docker image is build, you can run the container as before to make sure the app is working as expected:</p><pre><code class="language-bash">docker run -p 4000:3838 registry.gitlab.com/analythium/shinyproxy-hello/hello
</code></pre><h3 id="push-image">Push image</h3><p>Push the locally build Docker image to the container registry:</p><pre><code class="language-bash">docker push registry.gitlab.com/analythium/shinyproxy-hello/hello
</code></pre><p>The image tag should start with the registry name unless you are pushing to Docker Hub. When the image tag is not specified, Docker will treat the new image as <code>:latest</code> automatically. Read more about Docker tags and semantic versioning <a href="https://medium.com/@mccode/using-semantic-versioning-for-docker-image-tags-dfde8be06699">here</a>.</p><h2 id="whats-in-the-dockerfile">What's in the Dockerfile</h2><p>Let's review the <code>Dockerfile</code> line by line. The full Dockerfile reference can be found <a href="https://docs.docker.com/engine/reference/builder/">here</a>.</p><p>The <a href="https://docs.docker.com/engine/reference/builder/#from"><code>FROM</code> instruction</a> initializes a new build stage and sets the <em>base image</em>.<br>Take the latest <code>r-base</code> image from the <code>rocker</code> project (see on <a href="https://hub.docker.com/r/rocker/r-base">Docker Hub</a>):</p><pre><code class="language-dockerfile">FROM rocker/r-base:latest
</code></pre><p>The <code>LABEL</code> instruction is optional, it adds metadata to an image, e.g. who to contact in case of issues or questions:</p><pre><code class="language-dockerfile">LABEL maintainer="USER &lt;user@example.com&gt;"
</code></pre><p>The <a href="https://docs.docker.com/engine/reference/builder/#run"><code>RUN</code> instruction</a> executes the command in a new <em>layer</em> (a <a href="https://docs.docker.com/glossary/#layer">layer</a> is modification to the image) on top of the current image. The following command updates the base image with a couple of libraries that are required by Shiny and related R packages (system dependencies):</p>
```dockerfile
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    sudo \
    libcurl4-gnutls-dev \
    libcairo2-dev \
    libxt-dev \
    libssl-dev \
    libssh2-1-dev \
    &amp;&amp; rm -rf /var/lib/apt/lists/*
```
<p>The following <code>RUN</code> command uses the <a href="https://github.com/eddelbuettel/littler">littler</a> command line interface shipped with the <code>r-base</code> image to install the Shiny package and its dependencies:</p><pre><code class="language-dockerfile">RUN install.r shiny
</code></pre><p>The next command sets the options in the <code>Rprofile.site</code> file which are going to be loaded by the R session. These options specify the Shiny host and port that <code>runApp</code> will use.</p><p>Do not run containers as root in production. Running the container with root privileges allows unrestricted use which is to be avoided in production. Although you can find lots of examples on the Internet where the container is run as root, this is generally considered bad practice.</p><blockquote>Switching to the root <code>USER</code> opens up certain security risks if an attacker gets access to the container. In order to mitigate this, switch back to a non privileged user after running the commands you need as root. – <a href="https://github.com/hadolint/hadolint#rules">Hadolint rule DL3002</a></blockquote><p>The following command creates a Linux group and user, both called <code>app</code>. This user will have access to the app instead of the default root user:</p><pre><code class="language-dockerfile">RUN addgroup --system app \
    &amp;&amp; adduser --system --ingroup app app
</code></pre><p>You can read more about security considerations <a href="https://engineering.bitnami.com/articles/why-non-root-containers-are-important-for-security.html">here</a> and Dockerfile related code smells <a href="https://arxiv.org/abs/2103.12298">here</a>. Read about <a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/">best practices</a> and <a href="https://github.com/hadolint/hadolint#readme">linting rules</a> in general that can be helpful in reducing vulnerabilities of your containerized application.</p><p>The <a href="https://docs.docker.com/engine/reference/builder/#workdir"><code>WORKDIR</code> instruction</a> sets the working directory for subsequent instructions. Change this to the home folder of the <code>app</code> user which is <code>/home/app</code>:</p><pre><code class="language-dockerfile">WORKDIR /home/app
</code></pre><p>The <a href="https://docs.docker.com/engine/reference/builder/#copy"><code>COPY</code> instruction</a> copies new files or directories from the source (our <code>app</code> folder containing the R script files for our Shiny app) and adds them to the file system of the container at the destination path (<code>.</code> refers to the current work directory defined at the previous step):</p><pre><code class="language-dockerfile">COPY app .
</code></pre><p>The next command sets permissions for the <code>app</code> user:</p><pre><code class="language-dockerfile">RUN chown app:app -R /home/app
</code></pre><p>The <a href="https://docs.docker.com/engine/reference/builder/#user"><code>USER</code> instruction</a> sets the user name (or UID) and optionally the user group (or GID) to use when running the image:</p><pre><code class="language-dockerfile">USER app
</code></pre><p>The <a href="https://docs.docker.com/engine/reference/builder/#expose"><code>EXPOSE</code> instruction</a> tells Docker which ports the container listens on at runtime. Set this to the Shiny port defined in the <code>Rprofile.site</code> file:</p><pre><code class="language-dockerfile">EXPOSE 3838
</code></pre><p>Finally, the <a href="https://docs.docker.com/engine/reference/builder/#cmd"><code>CMD</code> instruction</a> closes off our <code>Dockerfile</code>. The <code>CMD</code> instruction provides the defaults for an executing container. There can only be one <code>CMD</code> instruction in a <code>Dockerfile</code> (only the last <code>CMD</code> will take effect). Our <code>CMD</code> specifies the executable (<code>"R"</code>) and parameters for the executable in an array. The <code>-e</code> option means you are running an expression that is <code>shiny::runApp('/home/app')</code>. The expression will run the Shiny app that we copied into the <code>/home/app</code> folder:</p><pre><code class="language-dockerfile">CMD ["R", "-e", "shiny::runApp('/home/app')"]
</code></pre><p>Build the image using <code>docker build</code> by specifying the tag (<code>-t</code>) and the context (<code>.</code> indicates the current directory):</p><pre><code class="language-bash">docker build -t registry.gitlab.com/analythium/shinyproxy-hello/hello .
</code></pre><p>You can test and push the locally build Docker image to the container as before.</p><h2 id="summary">Summary</h2><p>With the newfound ability to wrap any Shiny app in a Docker container, you'll be able to deploy these images to many different hosting platforms. Of course, there is a lot more to learn, e.g. about handling dependencies, persisting data across sessions and containers, and so on. We'll cover these use cases in due time. Until then, celebrate this milestone, check out further readings, and try to containerize some of your own Shiny apps.</p><h2 id="further-reading">Further reading</h2><ul><li><a href="https://ropenscilabs.github.io/r-docker-tutorial/">R Docker tutorial</a></li><li><a href="https://colinfay.me/docker-r-reproducibility/">Docker for R users</a></li><li><a href="https://journal.r-project.org/archive/2017/RJ-2017-065/RJ-2017-065.pdf">An Introduction to Rocker</a></li><li><a href="https://journal.r-project.org/archive/2020/RJ-2020-007/RJ-2020-007.pdf">The Rockerverse</a></li></ul>