If you haven’t heard of Docker, it is a system that allows projects to be split into discrete units (i.e. containers) that each operate within their own virtual environment. Each container has a blueprint written in its
Dockerfile that describes all of the operating parameters including operating system and package dependencies/requirements. Docker images are easily distributed and, because they are self-contained, will operate on any other system that has Docker installed, include servers.
When multiple instances/users attempt to start a Shiny App at the same time, only a single R session is initiated on the serving machine. This is problematic. For example, if one user starts a process that takes 10 seconds to complete, all other users will need to wait until that process has completed before any other tasks can be processed.
One of the benefits of deploying a containerised Shiny App is that each new instance will run in its own R session.
In this article, I will go through the steps that I took to deploy an app developed in Shiny onto a fully-functional web server. All the code here, plus some basic web-server configuration, can be found at this Telethon Kids GitHub repository.
Docker can be installed following the instructions here, making sure to also follow the post-installation instructions here. Docker-compose will also need to be installed by following these instructions here. For the example presented here, Docker was installed on an Ubuntu 18.04 OS in a Virtual Box; the same approach will be used for other apps running on an AWS EC2 instance.
I will be using the words image and container throughout this article. A Docker image is a functioning snapshot of the blueprint. A running image is called a container, which is operating by receiving, processing and sending information to the client or other containers.
Dockerfile contains the schematics of a Docker container, that is it is used to call a base image and define and customisations that need to be made for the specific application to run correctly.
This is the
Dockerfile that describes our Shiny App (i.e. the Default app when a new “Shiny Web App …” is created in RStudio):
FROM rocker/shiny:3.5.1 RUN apt-get update && apt-get install libcurl4-openssl-dev libv8-3.14-dev -y &&\ mkdir -p /var/lib/shiny-server/bookmarks/shiny # Download and install library RUN R -e "install.packages(c('shinydashboard', 'shinyjs', 'V8'))" # copy the app to the image COPY shinyapps /srv/shiny-server/ # make all app files readable (solves issue when dev in Windows, but building in Ubuntu) RUN chmod -R 755 /srv/shiny-server/ EXPOSE 3838 CMD ["/usr/bin/shiny-server.sh"]
This blueprint is using the base image
rocker/shiny built on R version 3.5.1. For some packages to run properly on an Ubuntu OS (the container’s base operating system) I needed to install
libv8 by adding the following lines to our
RUN apt-get update &&\ apt-get install libcurl4-openssl-dev libv8-3.14-dev -y &&\ mkdir -p /var/lib/shiny-server/bookmarks/shiny
The packages that the app depends on are also installed via. the
Dockerfile with the
RUN R -e "install.packages(c('shinydashboard', 'shinyjs', 'V8'))"
app.R file (i.e. the Shiny App) is copied from the shinyapps directory on the host PC to a folder inside the container image with the
COPY statement. See the GitHub repo for an example of the project directory structure.
I had some file permission issues while building on Ubuntu via. a Windows VM. To overcome this, all copied file permissions were changed with
chmod -R 755 to ensure they were readable and executable inside the container.
# Copy the app to the image COPY shinyapps /srv/shiny-server/ # Make all app files readable RUN chmod -R +r /srv/shiny-server/
The final two lines expose port 3838 to receive incoming traffic and tell docker how to start the app. With the
Dockerfile complete, images are easily built with the following one-liner:
docker build -t telethonkids/new_shiny_app ./path/to/Dockerfile/directory
and can be run as a standalone container with:
docker run --name=shiny_app --user shiny --rm -p 80:3838 telethonkids/new_shiny_app
and accessed in a browser at
The story doesn’t end there. ShinyProxy can be easily set up in another container to facilitate container creation. An image for a ShinyProxy container is defined by the following Dockerfile, this is an example from ShinyProxy’s GitHub repository for a containerised deployment:
FROM openjdk:8-jre RUN mkdir -p /opt/shinyproxy/ RUN wget https://www.shinyproxy.io/downloads/shinyproxy-2.1.0.jar -O /opt/shinyproxy/shinyproxy.jar COPY application.yml /opt/shinyproxy/application.yml WORKDIR /opt/shinyproxy/ CMD ["java", "-jar", "/opt/shinyproxy/shinyproxy.jar"]
The apps that are to be hosted are defined in the
application.yml file. This contains the information for ShinyProxy to launch Shiny Apps and needs to be copied into the container from the host with
COPY application.yml /opt/shinyproxy/application.yml.
The following code snippet has only one Shiny App, but multiple app configurations can be added to
proxy: title: Telethon Kids Institute Shiny Apps hide-navbar: false landing-page: / heartbeat-rate: 10000 heartbeat-timeout: 600000 port: 8080 docker: internal-networking: true specs: - id: shiny_app display-name: New Shiny App description: The default app when initiating a new shiny app via. RStudio container-cmd: ["/usr/bin/shiny-server.sh"] container-image: telethonkids/new_shiny_app container-network: tki-net container-env: user: "shiny" environment: - APPLICATION_LOGS_TO_STDOUT=false
This ShinyProxy set up listens for traffic on port 8080 and will time-out after a period of inactivity, which has been set to 10 minutes.
heartbeat-rate: 10000 heartbeat-timeout: 600000
internal-networking: true must be set because ShinyProxy is being run on a container on the same host as the Shiny App. The configuration for the each app is listed under
specs, which give instructions on how to launch the container, what Docker image to use to start the container and what network it should be listening on (note that the
container-network must be the same as the network listed in the
docker-compose.yaml file, more to come soon).
container-cmd: ["/usr/bin/shiny-server.sh"] container-image: telethonkids/new_shiny_app container-network: tki-net
We also have some environment variables that we want to set so the R session is being run for the user shiny and logs are not recorded.
container-env: user: 'shiny' environment: - APPLICATION_LOGS_TO_STDOUT=false
docker-compose.yml file contains a series of instructions that can be used to build and launch all the containers needed for deployment in a single or multi-container project. The benefit of this is that a complex network of containers and networks can be version controlled, and launched with one line of code.
version: "3.6" services: shinyproxy: depends_on: - nginx - influxdb image: telethonkids/shinyproxy container_name: tki_shinyproxy restart: on-failure networks: - tki-net volumes: - ./shinyproxy/application.yml:/opt/shinyproxy/application.yml - /var/run/docker.sock:/var/run/docker.sock ports: - 8080:8080 networks: tki-net: name: tki-net
networks to enable communication between containers. For ShinyProxy to communicate properly with the Shiny App, the network specified in
docker-compose.yml must be the same as the same as that listed in
application.yml. (Tip, if you’re using a docker-compose file to launch the app, don’t set up the docker network manually, see here for why).
Each container listed in
services: is named in
container_name, with the
Dockerfile being pointed to via. the
If the container was terminated with an error code, the
restart line instructs Docker what action is to be taken . The
networks variables lists the Docker network that the container has access to. Finally, the app is exposed on port 80.
The network used by all applications in this
docker-compose must match the network specified in the ShinyProxy
networks: telethonkids-net: name: telethonkids-net
With everything in place, the ShinyProxy image is built and the container(s) started with
docker-compose build and
docker-compose up -d. (Important, the app defined in
application.yml also be built). Containers are stopped and removed with
With the configurations listed here, a full-functioning Shiny App can be deployed to the internet with little extra configuration. Even simpler, if this setup can be cloned from GitHub and the files in “webapp/shinyapp” replaced by your Shiny App’s
Another app that I made used
CSS to do some custom formatting (only 4 tags). For some reason the style sheet was ignored by Chrome when run with ShinyProxy, but not when running standalone. The problem did not exist when tested on Firefox and Internet Explorer browsers. I think this had something to do with the
iframe used by ShinyProxy. This was resolved by putting the style in
app.R within a head tag and removing the reference to the external