I used shinyapps.io for my own shiny app. It’s a great service. You can deploy your app for free, test it and show it to other people. But there’s also a downside: The memory an app can use is limited.

So I was looking for another way to deploy my app. So I took a look at Docker.

What is Docker?

A Docker container contains all programs and libraries which are required for running a special application. So it’s easy to transfer or distribute it to another “Docker host” and run the application on it.

Two docker container are separated so they can’t interfere with another. They can only talk with each other using defined ports or directories.

So that’s a way to solve the problem with different package versions or versions of R.

How to put a shiny app into a Docker container?

You have to write a Dockerfile to describe how Docker builds an image which can be started as a Docker container. The Dockerfile is a recipe with several steps. If you change one step all steps before the changed step can be reused. The changed step and all steps after that one must be run again.

I found a nice description of a Dockerfile for a shiny app at this page of statworx.

Building a container with the right R Environment using Renv

The main problem is to install all the R packages needed by your shiny app into your Docker container. Statworx uses the new renv R package management tool from RStudio.

Renv can write all needed packages into a file called renv.lock. If you run renv::restore() these packages are installed again.

In practice some packages aren’t installed because some requirements on the OS-level aren’t met. You’ll get error messages which guide you to the Linux packages you should install into your container before installing the R packages.

Dockerfile

But how does my Dockerfile look like? Here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Base image https://hub.docker.com/u/rocker/
FROM rocker/shiny-verse:latest

# system libraries of general use
## install debian packages
RUN apt-get update -qq && apt-get -y --no-install-recommends install \
    libxml2-dev \
    libcairo2-dev \
    libsqlite3-dev \
    libpq-dev \
    libssh2-1-dev \
    unixodbc-dev \
    r-cran-v8 \
    libv8-dev \
    net-tools \
    libprotobuf-dev \
    protobuf-compiler \
    libjq-dev \
    libudunits2-0 \
    libudunits2-dev \
    libgdal-dev \
    libssl-dev

## update system libraries
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get clean

# copy necessary files
## renv.lock file
COPY /app/renv.lock ./renv.lock

# install renv & restore packages
RUN Rscript -e 'install.packages("renv")'
RUN Rscript -e 'renv::restore()'

## app folder
COPY /app ./app

# expose port
EXPOSE 3838

# run app on container start
# CMD ["R", "-e", "shiny::runApp('/app', host = '0.0.0.0', port = 3838)"]
CMD ["Rscript", "-e", "rmarkdown::run('/app/app.Rmd', shiny_args=list(host = '0.0.0.0', port=3838))"]

But let’s break it down into simple pieces:

The base image

1
2
# Base image https://hub.docker.com/u/rocker/
FROM rocker/shiny-verse:latest

That’s an image which contains a base R installation, tidyverse and the shiny server. This image uses Ubuntu as operating system.

Adding Ubuntu packages

So now we install all required (Ubuntu-) packages required for our R-packages:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# system libraries of general use
## install debian packages
RUN apt-get update -qq && apt-get -y --no-install-recommends install \
    libxml2-dev \
    libcairo2-dev \
    libsqlite3-dev \
    libpq-dev \
    libssh2-1-dev \
    unixodbc-dev \
    r-cran-v8 \
    libv8-dev \
    net-tools \
    libprotobuf-dev \
    protobuf-compiler \
    libjq-dev \
    libudunits2-0 \
    libudunits2-dev \
    libgdal-dev \
    libssl-dev

## update system libraries
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get clean

Installing R packages

Now we install all R packages required by our shiny app. That’s an optimization in respect to the statworx example I did here.

Statworx copies the whole app including the renv.lock file into the image in this next steps and installs the R packages.

I copy only the renv.lock file and install the packages. You’ll see the advantage soon.

1
2
3
4
5
6
7
# copy necessary files
## renv.lock file
COPY /app/renv.lock ./renv.lock

# install renv & restore packages
RUN Rscript -e 'install.packages("renv")'
RUN Rscript -e 'renv::restore()'

That’s the step you’ll get the most errors because of missing dependancies. It’s also the most time consuming step. So you want to run it only if it is really required.

Installing and running the shiny app

1
2
3
4
5
6
7
8
9
## app folder
COPY /app ./app

# expose port
EXPOSE 3838

# run app on container start
# CMD ["R", "-e", "shiny::runApp('/app', host = '0.0.0.0', port = 3838)"]
CMD ["Rscript", "-e", "rmarkdown::run('/app/app.Rmd', shiny_args=list(host = '0.0.0.0', port=3838))"]

Now we copy the shiny app into the image and run it. We also say that the app runs on port 3838 and declare this port to be visible from outside the container.

Interations

So now you can see the advantage of splitting the renv.lock file and the whole app: Once you have set up your R environment in your container and you do only changes to your shiny app you can reuse the first and time consuming steps of your container creating process. Docker only has to rerun the last steps: Copying your app and running it.

Reverse Proxy

When running your Docker container on a server you may want to use a reverse proxy such as nginx to map the local port to the standard port 80 resp. 443 and use tls-encryption to protect the traffic.