Deploying an R Shiny App on Heroku via GitHub Actions and Dockerfile to Access Private GitHub Repositories

Deploying an R Shiny App on Heroku when Github Secrets are Needed to access private GitHub repositories
R
Shiny
Heroku
GitHub Actions
Docker
Author

Jason Zivkovic

Published

February 24, 2023

Introduction

I’ve deployed a few R Shiny apps now on Heroku that have been containerised using Docker and run from a Github Action and found the process fairly seamless (well as seamless as Dev Ops for a hack goes). The approach worked wonderfully for installing public packages from CRAN and reading in data from public GitHub repositories.

This time though in my Heroku deployed R Shiny app, I needed a way to load in data from a private GitHub Releases repository AND install an R library that I’d written - which is also in a private GitHub repository.

This post is going to build on a super helpful post I’ve come across that has helped me on a few R Shiny app deployments on Heroku. The post Deploying Shiny Apps to Heroku with Docker and GitHub Actions by Peter Solymos can be found here.

The below instructions will also assume you have Docker installed on your machine, have set up a GitHub account, Heroku account, and on the heroku account have set up the dynos (app containers) you need.


Deploying a R Shiny app on Heroku using Docker and GitHub Actions

Peter’s super helpful post has served me well and almost did everything I needed in this specific situation. To recap everything in those instructions (and a few other steps to absolutely complete the end-to-end):

  1. Build your app, create a sub-directory called app/ and save the app in project/app/. Any other files you also need for your app (data, functions, environments, etc, it is easier if they are also saved in app/, unless you don’t need those files for the app to run, as explained in this post)
  2. For reproducibility, use renv or some other package manager to ensure a consistent environment. Call renv::init() to capture dependencies in the renv.lock file
  3. In your project root, create a Dockerfile and paste the below contents in there, replacing the value in LABEL maintainer to your own details:
# change `r-base:latest` to another valid version if you want to pin a specific R version
FROM rocker/r-base:latest

# change maintainer here
LABEL maintainer="Your Name <your.email.address.com>"

# add system dependencies for packages as needed
RUN apt-get update && apt-get install -y --no-install-recommends \
    sudo \
    libcurl4-gnutls-dev \
    libcairo2-dev \
    libxt-dev \
    libssl-dev \
    libssh2-1-dev \
    && rm -rf /var/lib/apt/lists/*

# we need remotes and renv
RUN install2.r -e remotes renv

# create non root user
RUN addgroup --system app \
    && adduser --system --ingroup app app

# switch over to the app user home
WORKDIR /home/app

COPY ./renv.lock .
RUN Rscript -e "options(renv.consent = TRUE);renv::restore(lockfile = '/home/app/renv.lock', repos = c(CRAN = 'https://cloud.r-project.org'), library = '/usr/local/lib/R/site-library', prompt = FALSE)"
RUN rm -f renv.lock

# copy everything inside the app folder
COPY app .

# permissions
RUN chown app:app -R /home/app

# change user
USER app

# EXPOSE can be used for local testing, not supported in Heroku's container runtime
EXPOSE 3838

# web process/code should get the $PORT environment variable
ENV PORT=3838

# command we want to run
CMD ["R", "-e", "shiny::runApp('/home/app', host = '0.0.0.0', port=as.numeric(Sys.getenv('PORT')))"]

OPTIONAL: To test in a local docker container:

  • Build the container using sudo docker build -t image_name . replacing image_name with anything you want to call the image. Don’t forget to add the . at the end of the docker build command
  • Then test the container using docker run -p 6543:3838 image_name and then visit 127.0.0.1:4000 to see your app in all its glory
  1. Log in to Heroku. In the dashboard, click on ‘New’ then select ‘Create new App’.
  2. Give a name (e.g. shiny-example, if available, this will create the app at https://shiny-example.herokuapp.com/) to the app and create the app
  3. In your Heroku dashboard, go to your personal settings
  4. Find your API key, click on reveal and copy it, you’ll need it later
  5. Go to the Settings tab of the GitHub repository, scroll down to Secrets and add your HEROKU_EMAIL and HEROKU_API_KEY as repository secrets
  6. In the project directory locally, create the directory .github/workflows/ and then create a yml file called deploy.yml (or you can call this anything really)
  7. Put the below in the deploy.yml file you created at step 6, remembering to change the heroku_app_name variable to the name of your app. Note, the building and pushing of the Docker image to the Heroku container registry is based on the akhileshns/heroku-deploy GitHub action:
name: Build Shiny Docker Image and Deploy to Heroku

on:
  push:
    branches:
      - main

jobs:
  app1:
    name: Build and deploy Shiny app
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Build and push Docker to Heroku
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_app_name: shiny-example
          appdir: "."
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_email: ${{ secrets.HEROKU_EMAIL }}
          usedocker: true

Your project should now look something like this:

+-- .Rproj.user
+-- .github/workflows
|   +-- deploy.yml
+-- project.Rproj
+-- .gitignore
+-- Dockerfile
+-- app
|   +-- app.R
|   +-- data-df.rds
|   +-- globals.R
+-- renv
+-- renv.lock
  1. To trigger a build, commit to your remote repository on GitHub, go to the Actions tab and you should see it starting to build. Hope for a green tick and your app should then be displayed at https://shiny-example.herokuapp.com/

Solving this problem: Accessing private GitHub repositories in a Dockerfile run from a GitHub Action

So as mentioned, the above section has served me well many times, but once I needed to access content from private repositories in a shiny app deployed on Heroku with a Dockerfile run on GitHub Actions, I came unstuck.

Here I will label the steps I took to get around this.

If you haven’t created a GitHub Personal Access Token (PAT) and given it permissions to access private repositories, do so now. Do this in the Settings menu of your GitHub account. Call it something other than GITHUB_PAT - for this example, we’ll name it PRIVATE_REPO_PAT.

Store the name of the PAT and the value somewhere secure as you’ll need this next.

Go and add that secret(s) in the app settings on Heroku in the section called ‘Config Vars’ here.

Then we need to update our deploy.yml file by adding the below to the end of deploy.yml:


          docker_build_args: |
            GITHUB_PAT
        env:
          GITHUB_PAT: ${{ secrets.PRIVATE_REPO_PAT }}

The full deploy.yml should now look like the below:

name: Build Shiny Docker Image and Deploy to Heroku

on:
  push:
    branches:
      - main
      - master

jobs:
  deploy:
    name: Build and deploy Shiny app
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Build and push Docker to Heroku
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          # this is the Heroku app name you already set up in dashboard
          heroku_app_name: nbl-r-shiny
          # app directory needs to be set relative to root of repo
          appdir: "."
          # secrets need to be added to the GitHub repo settings
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_email: ${{ secrets.HEROKU_EMAIL }}
          # don't change this
          usedocker: true
          docker_build_args: |
            GITHUB_PAT
        env:
          GITHUB_PAT: ${{ secrets.PRIVATE_REPO_PAT }}

Now we also need to update the Dockerfile by adding the below after the first FROM statement:

ARG GITHUB_PAT=default
ENV GITHUB_PAT=$GITHUB_PAT

The full Dockerfile should look like the below (again, remembering to change to your own maintainer details):

FROM rocker/shiny:4.1.0

# set env var
ARG GITHUB_PAT=default
ENV GITHUB_PAT=$GITHUB_PAT


# change maintainer here
LABEL maintainer="Your Name <your.email.address.com>"

# add system dependencies for packages as needed
RUN apt-get update && apt-get install -y --no-install-recommends \
    sudo \
    libcurl4-gnutls-dev \
    libcairo2-dev \
    libxt-dev \
    libssl-dev \
    libssh2-1-dev \
    && rm -rf /var/lib/apt/lists/*

# we need remotes and renv
RUN install2.r -e remotes renv

# create non root user
RUN addgroup --system app \
    && adduser --system --ingroup app app

# switch over to the app user home
WORKDIR /home/app

COPY ./renv.lock .
RUN Rscript -e "options(renv.consent = TRUE);renv::restore(lockfile = '/home/app/renv.lock', repos = c(CRAN = 'https://cloud.r-project.org'), library = '/usr/local/lib/R/site-library', prompt = FALSE)"
RUN rm -f renv.lock

# copy everything inside the app folder
COPY app .

# permissions
RUN chown app:app -R /home/app

# change user
USER app

# EXPOSE can be used for local testing, not supported in Heroku's container runtime
EXPOSE 3838

# web process/code should get the $PORT environment variable
ENV PORT=3838

# command we want to run
CMD ["R", "-e", "shiny::runApp('/home/app', host = '0.0.0.0', port=as.numeric(Sys.getenv('PORT')))"]

To test that this has worked in a local Docker container, simply run docker run --env GITHUB_PAT=ghp_1234 -p 6543:3838 image_name, replacing ghp_1234 with your actual value for PRIVATE_REPO_PAT and image_name with the actual Docker image name.

Finally, commit changes to your remote repository on GitHub, wait for your green build, go to the app URL and you should be up and running.


Hope you have found this helpful!

Acknowledgements

Special thanks to Peter Solymos again for the post listed in the intro.

Additionally, massive thanks to Steve Condylios and Tan Ho for their massive help getting to this solution.