Docker and ShinyProxy Setup

post thumb
Article
by Roman Plessl/ on 08 Oct 2019

Docker and ShinyProxy Setup

Based on one of our customer needs: If you have created one or more Shiny application and want to put them simple to production for multiple and / or selective users ShinyProxy is my favourite way to deploy these Shiny apps.

Scope

ShinyProxy allows you to run Shiny application at scale and with added authentication, authorization and customer separation.

Shiny allows us to:

  • scale to a bunch of concurrent users
  • fully decouple the Docker Container instances per End-User
  • authentication and authorization (with AWS Cognito, Auth0 or username / password tupels)
  • allocate resources (CPU, memory limits) per configured Shiny application
  • usage statistics for monitoring and accounting

We setup the ShinyProxy running on a Ubuntu 18.04 LTS machine running docker.

As the right configuration with Nginx, Let’s Encrypt, Docker and ShinyProxy was somehow tricky, we like to share our setup and configuration notes:

  • Single Node Docker Host (we are using Ubuntu 18.04)
  • Docker Community Editon
  • Docker Compose for start and handle multiple containers
  • ShinyProxy to managed and handle the Shiny Application Docker Containers.

So let’s start

Basic installation of Docker and Docker Compose

Installation of Docker and Docker Compose Script:

#!/bin/bash

# Installing Docker Community Edition
sudo apt-get remove docker docker-engine docker.io
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=$(([ $(uname -m) = "x86_64" ] && echo "amd64" ) || echo $(uname -m))] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install -y docker-ce

# Managing Docker as a non-root user
sudo usermod -aG docker $USER

# Configuring Docker to start on boot
sudo systemctl enable docker
sudo systemctl start docker

# Install docker-compose
sudo curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Configuration of Docker to be accessible by tcp on localhost

#!/bin/bash
echo "### Configure docker to be access also by tcp (needed for shiny-proxy) ###"

sudo mkdir -p /etc/systemd/system/docker.service.d/
cat <<EOF > override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -D -H tcp://127.0.0.1:2375 -H unix:// 
EOF
sudo cp override.conf /etc/systemd/system/docker.service.d/
sudo systemctl daemon-reload
sudo systemctl restart docker

Docker-Compose

We use docker-compose to spawn multiple docker containers and start als shinyproxy container:

version: "2"

services:
  nginx:
    restart: always
    image: nginx
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/etc/nginx/conf.d"
      - "/etc/nginx/vhost.d"
      - "/usr/share/nginx/html"
      - "./volumes/proxy/certs:/etc/nginx/certs:ro"
    labels:
      - com.centurylinklabs.watchtower.enable=true

  nginx-gen:
    restart: always
    image: jwilder/docker-gen
    container_name: nginx-gen
    volumes:
      - "/var/run/docker.sock:/tmp/docker.sock:ro"
      - "./volumes/proxy/templates/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro"
    volumes_from:
      - nginx
    entrypoint: /usr/local/bin/docker-gen -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    labels:
      - com.centurylinklabs.watchtower.enable=true

  letsencrypt-nginx-proxy-companion:
    restart: always
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: letsencrypt-nginx-proxy-companion
    volumes_from:
      - nginx
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./volumes/proxy/certs:/etc/nginx/certs:rw"
    environment:
      - NGINX_DOCKER_GEN_CONTAINER=nginx-gen
    labels:
      - com.centurylinklabs.watchtower.enable=true

# Watchtower is a process for watching your Docker containers and automatically
# updating and restarting them whenever their base image is refreshed.
  watchtower:
    restart: always
    image: v2tec/watchtower
    container_name: watchtower
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    environment:
      - WATCHTOWER_NOTIFICATIONS=slack
      - WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://hooks.slack.com/services/<CHANGEME>
    # Look for new images every midnight and Removes old images
    command:
      --label-enable --schedule "0 0 9,15 * * *" --cleanup
    labels:
      - com.centurylinklabs.watchtower.enable=true

### PRUNUX APPLICATIONS ###
  
  shinyproxy-demo:
    restart: always
    image: openanalytics/shinyproxy-demo
    container_name: shinyproxy-demo
    environment:
      - VIRTUAL_HOST=shinyproxy-demo.prunux.ch
      - VIRTUAL_NETWORK=nginx-proxy
      - VIRTUAL_PORT=8080
      - LETSENCRYPT_HOST=shinyproxy-demo.prunux.ch
      - LETSENCRYPT_EMAIL=not_an_valid_email_address@prunux.ch
    labels:
      - com.centurylinklabs.watchtower.enable=true

  shinyproxy:
    restart: always
    image: prunux/shinyproxy:0.1
    container_name: shinyproxy
    ports:
    - 18080:18080
    networks:
    - default
    - prunux-shiny-net
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    environment:
    - VIRTUAL_HOST=shiny-apps.prunux.ch
    - VIRTUAL_NETWORK=nginx-proxy
    - VIRTUAL_PORT=18080
    - LETSENCRYPT_HOST=shiny-apps.prunux.ch
    - LETSENCRYPT_EMAIL=not_an_valid_email_address@prunux.ch

networks:
  prunux-shiny-net:
    external:
      name: prunux-shiny-net

ShinyProxy Docker Container Configuration and Build

And we build the shinyproxy container with to following settings and script

proxy: 
  title: prunux.ch Application Entry Page
  logo-url: https://www.prunux.ch/images/logo.svg
  landing-page: /
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 18080
  authentication: simple
  admin-groups: admins
  users:
  - name: picard
    password: SizeTheTime
    groups: admins
  - name: riker
    password: IAmTheNumber1
    groups: admins
  - name: borg
    password: ResistanceIsFutile
  - name: quark
    password: MoneyMoneyMoney
  docker:
    internal-networking: true
  specs:
  - id: 01_hello
    display-name: Hello Application
    description: Application which demonstrates the basics of a Shiny app
    container-cmd: ["R", "-e", "shinyproxy::run_01_hello()"]
    container-image: openanalytics/shinyproxy-demo
    container-network: prunux-shiny-net
  - id: 02_prunuxstat
    display-name: Prunux Statistics Application
    description: Prunux Statistics Application
    container-cmd: ["R", "-e", "shiny::runApp('/root/app_prunux_statistics')"]
    container-image: prunux/shinystat:0.22
    container-network: prunux-shiny-net
  - id: 09_tabsets
    container-cmd: ["R", "-e", "shinyproxy::run_06_tabsets()"]
    container-image: openanalytics/shinyproxy-demo
    container-network: prunux-shiny-net
    access-groups: admins

logging:
  file:
    shinyproxy.log

Script the building of the ShinyProxy Docker Container:

#!/bin/bash
docker build -t prunux/shinyproxy:0.1 .
docker save --output prunux-shinyproxy-0.1.tar prunux/shinyproxy:0.1
rsync -av --compress --progress prunux-shinyproxy-0.1.tar ubuntu@shiny-apps.prunux.ch:/tmp/

Script the import of the ShinyProxy Docker Container:

#!/bin/bash
cat prunux-shinyproxy-0.1.tar | docker import - prunux/shinyproxy:0.1

Finally … it run’s!

Test with your HTTPS URL and debug with the Docker Logs and ShinyProxy Logs (i.e. open https://shiny-apps.prunux.ch in your webbrowser).

Enjoy your unicorn ride of running Shiny Applications with ShinyProxy, Let’s Encrypt, Docker and Docker-Compose!