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
renv
or some other package manager to ensure a consistent environment. Callrenv::init()
to capture dependencies in therenv.lock
file - In your project root, create a
Dockerfile
and paste the below contents in there, replacing the value inLABEL 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 .
replacingimage_name
with anything you want to call the image. Don’t forget to add the.
at the end of thedocker build
command - Then test the container using
docker run -p 6543:3838 image_name
and then visit127.0.0.1:4000
to 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_EMAIL
andHEROKU_API_KEY
as 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.yml
file you created at step 6, remembering to change theheroku_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 theakhileshns/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
- 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.