---
title: "Dockerizando Shiny"
output: github_document
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```

## Introducción

Muchas veces, la versión estándar de los servidores gratuitos de Shiny no alcanzan para nuestras hermosas creaciones en R. La limitación que más me molestaba era la de las 5 instancias activas simultáneamente. Para entornos de test quizá no estaba mal, pero una vez que la aplicación llega a ámbitos más profesionales donde probablemente más personas utilicen la app al mismo tiempo es más difícil.  

En este breve tutorial llevaremos una apliación de Shiny a un repositorio Docker y de allí mismo haremos un deploy a un servicio de Google Cloud con más RAM y CPUs disponibles.

## Ingredientes

- Una aplicación Shiny, .Rmd, flexdashboard (como en este ejemplo), etc.
- Docker y una cuenta Docker para subir la imagen a un repositorio.
- Cuenta en Google Cloud y las APIs básicas activas (el proceso es bastante intuitivo).

## Instalando Docker

Para instalar Docker, se obtiene el archivo desde la página oficial y se instala normalmente. En OSX, como es mi caso, después de descargar el .dmg se instala de la siguiente manera desde la terminal:

```{bash, eval = FALSE}
sudo hdiutil attach Docker.dmg
sudo /Volumes/Docker/Docker.app/Contents/MacOS/install
sudo hdiutil detach /Volumes/Docker
```

## Creando el Dockerfile

El archivo Dockerfile es un conjunto de líneas de texto que configuran los paquetes que Docker va a utilizar para generar el entorno de nuestra aplicación y es donde también le daremos "órdenes" para ejecutar la app.

En mi caso, creé una carpeta que se llama "Docker" con dos elementos: una carpeta llamada "R" donde está todo lo relacionado a la aplicación Shiny y el propio archivo "Dockerfile" (así, sin puntos, ni extensiones, ni cosas raras).

En mi caso, el archivo Dockerfile contiene las siguientes líneas, comentadas punto a punto para entender mejor:

```{bash, eval = FALSE}
FROM rocker/shiny:latest # Se instala el entorno Shiny y R

RUN apt-get update -qq && apt-get -y --no-install-recommends install \ # Se instalan librerías necesarias 
	libgdal-dev \ # Ojo que probablemente necesites más o menos
	libproj-dev \ # depende de los paquetes que uses
	libgeos-dev \
	default-libmysqlclient-dev \
	libmysqlclient-dev \
	libudunits2-dev \
	netcdf-bin \
	libxml2-dev \
	libcairo2-dev \
    libsqlite3-dev \
    libpq-dev \
    libssh2-1-dev \
    unixodbc-dev \
    libcurl4-openssl-dev \
    libssl-dev
    
RUN apt-get install pandoc # Tenía problemas con Pandoc e instalarlo por separado lo solucionó

RUN apt-get update && \       # Actualizar paquetes y limpiar
    apt-get upgrade -y && \
    apt-get clean

# Se instalan los paquetes necesarios
# notar que ya estamos ejecutando COMANDOS DE R dentro del Docker

RUN R -e "install.packages(pkgs=c('shiny','tidyverse',
'flexdashboard', 'leaflet', 'sf', 'ggalluvial', 
'plotly', 'rmarkdown'), repos='https://cran.rstudio.com/')" 

RUN R -e "install.packages(pkgs=c('devtools'), 
repos='https://cran.rstudio.com/')" 

RUN R -e  "devtools::install_github('rstudio/leaflet')"

RUN mkdir /root/app # Creamos la carpeta de la app

COPY R /root/shiny_save # Copia la carpeta "R" a la carpeta shiny_save
 
EXPOSE 3838 #Importantísimo: asignarle un puerto de comunicación a la app.
#Después no vamos a poder hacer nada si no se expone el puerto

# Se corre la app (en este caso, al ser un flexdashboard 
# tengo que ejecutarlo con rmarkdown, si no sería shiny::runApp(...)). 
# Como argumento extra le paso el puerto abierto.
CMD ["R", "-e", "rmarkdown::run('/root/shiny_save/dash_v2.Rmd', 
shiny_args=list(host='0.0.0.0', port=3838))"] 

```

## Construcción de la imagen

Una vez que tengamos el dockerfile y la carpeta con todos los archivos que necesita la app para correr, pasamos a construir la imagen de Docker propiamente dicha. Para esto, abrimos la consola y ejecutamos:

```{bash, eval = FALSE}
docker login
```
Nos pedirá el usuario y la contraseña de Docker. La escribimos y luego:

```{bash, eval = FALSE}
docker build -t nombreapp .
```

Ese comando construirá una imagen llamada "nombreapp" con los parámetros del dockerfile (para eso el punto al final). Esta imagen contiene el entorno R y todo el código que hemos usado, dentro de un sistema Linux.  
Es por esto que probablemente tarde bastante en ejecutarse la primera vez (a mí me lleva aproximadamente 3 horas), obviamente dependiendo la complejidad de la app. Si hacemos modificaciones al código de R de la aplicación y pusheamos los cambios (ejecutando los mismos parámetros), la construcción tarda muchísimo menos ya que la mayoría de las capas  del entorno son las mismas y no se modifican. Esto es algo que me encanta de Docker. El trabajo se divide en "capas", por lo que si corregís una parte pequeña del código, el build y el push de la app tardan segundos.


```{bash, eval = FALSE}
docker tag nombreapp usuario/nombreapp:latest
```

"Tagueamos" la app dentro del repositorio público que estamos por crear. Es como decir que lo estamos "seleccionando" para subirlo. El repositorio se llamará usuario/nombreapp. La última parte ":latest" es un nombre que podemos poner para identificar a la última versión de la app. Lo podemos ir modificando para cada versión.

```{bash, eval = FALSE}
docker push usuario/nombreapp:latest
```

Se pushea la imagen al repositorio, tal como si fuera GitHub.

## Deploy en Google Cloud 

  Una vez creado el proyecto, procedemos a abrir la consola de comandos dentro de la misma Web de Google.
  Lo primero que debemos hacer es loguearnos con nuestras credenciales de Docker. 

```{bash, eval = FALSE}
docker login
```

  Posteriormente, "traemos" la imagen del Docker a nuestro proyecto de Google Cloud con los últimos cambios.
  
```{bash, eval = FALSE}
docker pull usuario/nombreapp:latest
```
Luego, análogamente a lo que hacíamos en Docker, se "taguea" la imagen a pushear, lo único es que esta vez no pusheamos al repo de Docker sino al container de imágenes de Google Cloud. Reemplazá PROJECT_ID por el ID de tu proyecto.

```{bash, eval = FALSE}
docker tag usuario/nombreapp:latest gcr.io/PROJECT_ID/usuario/nombreapp:latest
```

Por último, pusheamos la imagen. 

```{bash, eval = FALSE}
docker push gcr.io/PROJECT_ID/usuario/nombreapp:latest
```

## Creación de la instancia del servicio

Ya casi terminamos. Luego de pusheada la imagen, en la pestaña de Google Cloud Run clickeás "Crear Servicio". Esto nos habilitará una máquina virtual para hacer el deploy y que podamos compartir la app a un montón de gente.  

Donde dice "Url de la imagen" poné seleccionar y se van a desplegar las opciones de imagen. Nos va a aparecer la imagen "nombreapp" con la versión "latest". Usamos esa y completamos los parámetros con lo que necesite el proyecto.

**IMPORTANTE:** En la parte de más abajo donde dice "CONTENEDOR", completá dentro del campo "Puerto del contenedor" el puerto 3838 o el que hayas elegido en la construcción del Dockerfile, ya que sin esto Google va a enrutar mal el puerto generando errores.

Esperás unos segundos a que Google cree la instancia y listo! Ya está la app deployada en Google Cloud Run. Cualquiera va a poder acceder a ella a través de la URL que te muestra arriba del panel. 


