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):
- Build your app, create a sub-directory called
app/and save the app inproject/app/. Any other files you also need for your app (data, functions, environments, etc, it is easier if they are also saved inapp/, unless you don’t need those files for the app to run, as explained in this post) - For reproducibility, use
renvor some other package manager to ensure a consistent environment. Callrenv::init()to capture dependencies in therenv.lockfile - In your project root, create a
Dockerfileand paste the below contents in there, replacing the value inLABEL maintainerto 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 .replacingimage_namewith anything you want to call the image. Don’t forget to add the.at the end of thedocker buildcommand - Then test the container using
docker run -p 6543:3838 image_nameand then visit127.0.0.1:4000to see your app in all its glory
- Log in to Heroku. In the dashboard, click on ‘New’ then select ‘Create new App’.
- 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 - In your Heroku dashboard, go to your personal settings
- Find your API key, click on reveal and copy it, you’ll need it later
- Go to the Settings tab of the GitHub repository, scroll down to Secrets and add your
HEROKU_EMAILandHEROKU_API_KEYas repository secrets - In the project directory locally, create the directory
.github/workflows/and then create a yml file calleddeploy.yml(or you can call this anything really) - Put the below in the
deploy.ymlfile you created at step 6, remembering to change theheroku_app_namevariable to the name of your app. Note, the building and pushing of the Docker image to the Heroku container registry is based on theakhileshns/heroku-deployGitHub 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: trueYour 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
- 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_PATThe 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.