Deploying Shiny Apps to Heroku with Docker From the Command Line

Heroku is a popular option for hosting and scaling apps without managing infrastructure. In this post, you are going to learn how to deploy a dockerized Shiny application to Heroku using the Heroku Command Line Interface (CLI).

What is Heroku

Heroku is a cloud platform-as-a-service (PaaS) that lets you deploy, run and manage applications. Heroku is also part of the Salesforce Platform, enabling enterprises to store and leverage customer data in Salesforce for full-cycle CRM engagement.

An application (app) is the combination of its source code and the dependency description that determines how to build and run the application. The build mechanism is typically language-specific and is based on so-called Buildpacks.

The Heroku landing page: R is not officially supported 

Because R is not one of the officially supported languages for Heroku, Buildpacks for R are all community maintained and might lag (e.g. this one has R 3.4.3). This R Buildpack GitHub repository is quite recent and supports packrat or renv based workflows for Shiny and Plumber.

The main caveat for these R Buildpacks is of course not that these are community maintained. Huge thanks to the maintainers for simplifying our lives and doing the heavy lifting for everyone else! The main caveat in their own words is that:

If any of your R packages dependend on system libraries which aren't included by Heroku, such as libgmp, libgomp, libgdal, libgeos and libgsl, you should use the Heroku container stack together with heroku-docker-r instead. – Maintainers of heroku-buildpack-r

What this means is that it is recommended to use a Docker-based stack to handle all the finicky system dependencies. As you have seen before, a Docker-based workflow with Shiny gives us an opportunity for local testing before deployment, which might also be very useful.

Heroku is really well suited for a Docker-based workflow because all Heroku applications run in a collection of lightweight Linux containers called dynos.

⚠️
Starting November 28, 2022, free Heroku Dynos, free Heroku Postgres, and free Heroku Data for Redis will no longer be available – see this FAQ for details.

Prerequisites

To follow this tutorial, you'll need to sign up for Heroku. It is free, to begin with. Read more about the general Heroku pricing, the dyno types, and free dyno hours.

Account verification and credit card info are not required for Heroku, but unverified accounts have limitations. For example the number of apps (5 vs 100), custom domains, etc. You can still be on the free plan after verification, so it might make sense to do so. It is up to you.

You will need git to be installed locally to be able to make changes to the git repository we are going to create.

To be able to communicate with Heroku from the command line, you'll use the Heroku Command Line Interface (CLI). Read more about how to install it here. Use heroku --version to test if the CLI is ready to be used. Then type heroku login which will prompt you to type in credentials. Next time the CLI will log you in automatically.

It goes without saying that you will need R, shiny, and the Docker Engine installed too.

The Shiny app

Let's create a new folder called heroku-hello and inside a Dockerfile:

mkdir heroku-hello
cd heroku-hello
touch Dockerfile

We will use the dockerized Hello example. This is an existing image that we use as a parent image in the Dockerfile:

FROM registry.gitlab.com/analythium/shinyproxy-hello/hello

ENV PORT=3838
CMD ["R", "-e", "shiny::runApp('/home/app', host = '0.0.0.0', port=as.numeric(Sys.getenv('PORT')))"]

The port settings and the CMD part is different from previous Dockerfiles that we used. The port number is passed as an environment variable by the Heroku container runtime. Therefore the runApp() command needs to pick up the port number using Sys.getenv('PORT'). The reason we add ENV PORT=3838 is to help with local testing.

If you want to apply this to your own app, you can

  • use the dockerized version of your app as a parent image as we did above;
  • or you can edit the Dockerfile to rely on the $PORT variable instead of hard coding and exposing a specific port.

Let's test the app:

docker build -t heroku-hello .
docker run -p 3838:3838 heroku-hello

Visit 127.0.0.1:3838 to see the familiar purple histogram with the sample size slider.

Deployment

Create a heroku.yml (touch heroku.yml) file in your application's root directory. The following example heroku.yml specifies the Dockerfile to be used to build the image for the app’s web process:

build:
  docker:
    web: Dockerfile

Create the application

This is the point where you have to commit your changes if any. Only repositories with new commits will deploy once the app is already on Heroku:

# Initialize the local directory as a Git repository
git init -b main

# Add the files and stage them for commit
git add .

# Commit tracked changes and sign-off the message
git commit -s -m "First commit"

Create the Heroku application with the container stack:

heroku create --stack=container

The command will give you the application URL:

Creating app... done, ⬢ morning-plateau-34336, stack is container
https://morning-plateau-34336.herokuapp.com/ | https://git.heroku.com/morning-plateau-34336.git

You might have noticed that a Heroku git remote is also added for the application to track changes.

You can configure an existing application to use the container stack using heroku stack:set container.

Deploy the application

Deploy your application to Heroku, replace main with your branch name if it is different:

git push heroku main

This will trigger a git push and a docker build on a remote Heroku server and docker push to the Heroku container registry:

Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 1.09 KiB | 1.09 MiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Compressing source files... done.
remote: Building source:
remote: === Fetching app code
remote:
remote: === Building web (Dockerfile)
remote: Sending build context to Docker daemon  3.072kB
remote: Step 1/3 : FROM registry.gitlab.com/analythium/shinyproxy-hello/hello
remote: latest: Pulling from analythium/shinyproxy-hello/hello
remote: 4363cc522034: Pulling fs layer

...

remote: Successfully built 8b891983b438
remote: Successfully tagged 6dc2e345582f143cd104cf98fec788c65e9a72a6:latest
remote:
remote: === Pushing web (Dockerfile)
remote: Tagged image "6dc2e345582f143cd104cf98fec788c65e9a72a6" as "registry.heroku.com/morning-plateau-34336/web"
remote: Using default tag: latest
remote: The push refers to repository [registry.heroku.com/morning-plateau-34336/web]
remote: cec4817fd20b: Preparing

...

remote: Verifying deploy... done.
To https://git.heroku.com/morning-plateau-34336.git
 * [new branch]      main -> main

Open the app in your browser using the heroku open command:

Log into your Heroku account and see the application listed in your dashboard. You can set up a custom domain for the app under the applications settings.

Note that there is no free SSL on custom domains, these will be served via HTTP and not HTTPS for apps that are using free resources.

If you want extra security, you have 3 options:

  • upgrade to a paid Heroku tier;
  • use Cloudflare;
  • serve the app via the original secure app URL and an iframe.

Conclusions

Heroku is really a no-hassle solution for hosting web applications, including dockerized Shiny apps. Using Docker makes it easy to rely on an already familiar workflow with very minimal modifications. The Heroku CLI is a powerful declarative tool that plays nicely with your git-based workflow.

⚠️
Starting November 28, 2022, free Heroku Dynos, free Heroku Postgres, and free Heroku Data for Redis will no longer be available – see this FAQ for details.

Further reading