How to deploy an R Shiny application to Google Cloud Platform (GCP) using Docker and Github Actions

R Shiny
Docker
GitHub Actinos

Learn how to effortlessly deploy R Shiny apps on Google Cloud Platform using Docker and GitHub Actions for seamless integration and efficient application management.

Author
Affiliation

Michal Lauer

Published

August 8, 2023

Modified

August 8, 2023

Motivation

Once you know R and Shiny, running your app on your local computer is quite easy. You can click the green play button in RStudio or use runApp(shinyApp(...)) from the {shiny} package. However, things get complicated if you want to share your app with your colleagues, friends, or significant other. It would help to have a server where your app will run so others can access it. In the current cloudy environment, this is much easier to do than some years ago. Your first option can be shinyapps.io, which offers free deployment up to some capacity. If you want to scale up, shinyapps offer paid tiers as well.

If you prefer more general solutions, you can look at the three biggest cloud provides we have - Amazon Web Service (AWS), Microsoft Azure, and Google Cloud Platform (GCP). Here, things get a little more complicated because they are not mainly for Shiny apps, and you need to learn new things, such as Docker, CLI tools specific for each platform and how your platform works.

Prerequisities

In this post, I aim to introduce a reproducible code which deploys your Shiny application to Google Cloud Platform using Docker and GitHub Actions. Before reading on, you should be comfortable with using Git, GitHub, and Shiny application development. You should know how to create a local Git repository and push your local code on GitHub.

Step 1: Creating the application and activating {renv}

The first step is to create a shiny application. You should have either app.R file or ui.R and server.R files. You should also set up your code repository with Git and Github repo. If you have not yet activated or used the {renv} package, I strongly recommend using it now. Activating it is as simple as running two commands in your R console. Furthermore, it enables an easy package installation when the app is deployed.

About {renv}

The {renv} package is very useful when you need a reproducible environment. It ‘snapshots’ your library dependencies and stores them in a renv.lock file. If you need to restore your project (let’s say on a different machine), you can just run renv::restore() to the same packages with the same versions.

If your app is already built without {renv}, run

install.packages("renv")
renv::init()

and press 1. This will create a local renv folder in which will now live your library and the renv.lock file. Anytime you change, add, remove, or update some of your packages, you need to run renv::snapshot() to update your renv.lock file.

Package installation

If you use {renv} \(\geq\) 1.0.0, calling renv::install(...) to install new packages will automatically update your renv.lock file.

{renv} is a powerful tool for data analysis, scientific research, and reproducible environments. To learn more about it, visit the official Introduction to renv page. Note that some packages try to do the same thing (such as packrat), but {renv} is becoming an industry standard. It is also developed by RStudio which ensures bug fixes, new features, compatibility with other tools, and quality.

Step 2: Creating a Dockerfile

Imagine now that you want to deploy your app. Will every cloud service - AWS, Azure, GCP, or shinyapps - have the same setup? Same operating system? Same installed software? Same hardware support? I could go on and on, but the setup on every platform is different. It would be great to have some intermediate steps that would ‘normalize’ your app, which could then be uploaded to any cloud service.

And there is! You can bundle your app into a container. This container is then deployed to a cloud service - such as GCP - which runs it. The container can be created using a Dockerfile (probably because the containers are called Dockers).

About Dockers

Docker is a great tool that can create containers of more things than a shiny app. The capabilities of Docker and related technologies, such as Kubernetes, are far greater to be explained in a single post. For further reading, I suggest some online courses or documentation.

You don’t need to be an expert Docker user to create a basic Dockerfile. If you only want to deploy your app to GCP, you can use the following code:

# Use shiny image
FROM rocker/shiny:4.3.0

# Update system libraries
RUN apt-get update -qq && apt-get -y --no-install-recommends install \
    libcurl4-openssl-dev \
    libssl-dev

# Set shiny working directory
RUN rm -rf /srv/shiny-server/*
WORKDIR /srv/shiny-server/

# Copy Shiny files
COPY /ui.R ./ui.R
COPY /server.R ./server.R
COPY /global.R ./global.R
COPY /R ./R
copy /www ./www

# Copy renv.lock file
COPY /renv.lock ./renv.lock

# Download renv and restore library
ENV RENV_VERSION 1.0.0
RUN Rscript -e "install.packages('remotes', repos = c(CRAN = 'https://cloud.r-project.org'))"
RUN Rscript -e "remotes::install_github('rstudio/renv@v${RENV_VERSION}')"
ENV RENV_PATHS_LIBRARY renv/library
RUN Rscript -e 'renv::restore()'

# Expose port
EXPOSE 3838

To use it, create in your root directory a file called Dockerfile (without any extension) and paste the yaml code. This chunk automatically

  • uses a predefined Shiny image to run your app,
  • updates system libraries,
  • updates working directory and copies relevant files and folders,
  • copies renv.lock and activates {renv}, and
  • exposes the app on port 3838.

Customization

Although this Dockerfile works, some things can be customized. Here, I will describe how to update this dockerfile and customize your Shiny app. If you use a different R version than 4.3.0, change the version on the first line. For example, with R version 4.2.0, you would use

# Use shiny image
FROM rocker/shiny:4.2.0

...

To see all available versions (tags), visit the official rocker docker hub.

I build my apps in three separate files:

  • ui.R holds the user interface,
  • server.R holds the server function, and
  • global.R holds general code for every session (such as library(...) calls).

Furthermore, I create all my functions in the R/ folder and add all my assets to the www/ folder. Since all five files and folders are necessary for my app, I need to copy all of them to the container. If you use only one file, for example, app.R, you would need only one file to copy - one line of code.

...

# Copy Shiny files
COPY /app.R ./app.R

...

Finally, you can change the version of the {renv} you use. Although I strongly recommend you to use the latest version (1.0.0 now), you can change it with

...

# Download renv and restore library
ENV RENV_VERSION 0.17.0

...

If you installed Docker, you can test this setup using commands

docker build -t my-shiny-app .
docker run -p 3838:3838 my-shiny-app

You can then visit localhost:3838 to see your shiny app running!

Step 3: Creating a workflow

The final step to full automation is a workflow. Once you deploy your Dockerfile and workflow to your GitHub repo, your app will be automatically deployed. If you are not familiar with GitHub Actions, I would again suggest watching online tutorials or reading through the official documentation.

In your root project directory, create folder .github and the following sub-folder workflows. Here, create a file called deploy.yml.

./.github/workflows/deploy.yml

After you create the file, paste there the following GitHub Actions workflow configuration.

name: Deploy to GCP
on: push
jobs:
  deploy:
    needs: tests
    env:
      IMAGE_NAME: gcr.io/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GCP_APP_NAME }}
      GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }}
      GCP_APP_NAME: ${{ secrets.GCP_APP_NAME }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - id: auth
        uses: google-github-actions/auth@v1
        with:
          credentials_json: ${{ env.GCP_CREDENTIALS }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      - name: Docker auth
        run: gcloud auth configure-docker europe-west3-docker.pkg.dev

      - name: Configure Docker
        run: gcloud auth configure-docker --quiet

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE_NAME }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy Docker image
        run: |
          gcloud run deploy ${{ env.GCP_APP_NAME }} \
          --image ${{ env.IMAGE_NAME }} \
          --region europe-west3 \
          --platform managed \
          --port=3838 \
          --allow-unauthenticated

In this file, only one option must be customized - the region. Depending on where you want to deploy your app, change the europe-west3 part to whatever supported location you want. I want to deploy in Frankfurt; hence I use europe-west3. If you want to customize your code in a more advanced way, refer to the official GCP SDK.

A final step to make this work is to update your GitHub repo secrets. These can hold sensitive information, such as API keys, passwords, or database names. In our case, GitHub secrets hold three pieces of information:

  • GCP_PROJECT_ID - ID of your project in GCP
  • GCP_APP_NAME - name of your app
  • GCP_CREDENTIALS - JSON-like structure which contains service account API

You can add these secrets when you open your GitHub repo and navigate to Settings -> Secrets and variables - Actions. For example, this is how my repository looks like:

Step 4: Putting it all together

Everything is now done, and you can push your new Dockerfile to your GitHub repo. Once you do this, open your Actions tab folder and see how your app is being deployed!

Practical example

For a practical example of how all this works in practice, see this GitHub repo which holds my (for this purpose irrelevant) Shiny application that is being automatically deployed and pushed to GCP.

That’s it! I hope this helped, and see you around! :)