Hosting ASP.NET Core applications on Heroku using Docker

Traditionally the way to host applications on Heroku is with a Buildpack, and while there are a few options for hosting ASP.NET Core apps with a Buildpack, I wanted to try their new Docker Container Registry and Runtime.

Hosting an ASP.NET Core application this way is probably better in the long run because you're using an official Microsoft image as your base and your deployment isn't tied to Heroku. You could use your application image anywhere Docker is supported.

Configuration

After following the guide on the Heroku Dev Center, the biggest stumbling block I had was passing the PORT env to the application.

At first, I tried using the CommandLine configuration extension, so I could pass the port into the Dockerfile CMD like in the Heroku demo application. Passing --server.urls http://*:$PORT as a command line argument to the application worked in dev, but as a published .NET project in a built Docker image, the application was still trying to bind to port 80, and I wasn't immediately able to figure out why in production it was not respecting the --server.urls flag.

The workaround I went with was using the EnvironmentVariables configuration extension and pass the ASPNETCORE_URLS env variable in the Dockerfile CMD.

FROM microsoft/aspnetcore:1.1.0

RUN adduser --disabled-password deployuser  
USER deployuser

WORKDIR /app  
COPY . .

CMD ASPNETCORE_URLS=http://*:$PORT dotnet HeroicHaiku.dll  

Heroku considerations

The constraints that Heroku has will probably make you consider some Docker best practices when deploying your application, such as:

  • Run as a non-privileged
  • Be mindful of the size of your image

Commands in the Dockerfile need to be run as a non-root user when deploying to Heroku. When trying to run dotnet restore to the Dockerfile, I ran in to a permissions issue trying to restore as a non-root user.

Running dotnet publish locally and then building an image from the published directory is probably a best practice, but it also gets around the permission issue.

Also, Heroku has limits on the size of the slug of an application and says that Docker images run the same way as slugs do on dynos and with the same constraints. So, I used the microsoft/aspnetcore Docker image as a base image to keep the application image small.

microsoft/aspnetcore-build 927.7 MB
microsoft/dotnet 608.5 MB
microsoft/aspnetcore 266.8 MB

The application image I pushed to the registry ended up being 276.1 MB.

Deployment

Compared to the traditional Heroku workflow of git push heroku master, deploying a Docker-based application to Heroku is a multi-step process:

  • Publish the ASP.NET Core project
  • Build a Docker image
  • Push it to the registry

However, it can be easily scripted. First, the Heroku Toolbelt is required to get an auth token, but then logging in is just like logging in to any other Docker registry, except you use _ for your username and your Heroku auth token for the password.

heroku login  
docker login --username=_ --password=$(heroku auth:token) registry.heroku.com  

And finally, deploying to Heroku is just as easy as pushing your image to their registry. Here is what my deploy script looks like:

#!/usr/bin/env sh

APP_NAME=heroichaiku

# Build
dotnet publish  
docker build bin/Debug/netcoreapp1.1/publish -t $APP_NAME

# Publish
docker tag $APP_NAME registry.heroku.com/$APP_NAME/web  
docker push registry.heroku.com/$APP_NAME/web  

You can find the full project on Github and try out the running project at heroichaiku.herokuapp.com. It's hosted on the free plan so the first connection might take a while as it boots up.

Next step

The obvious next step is adding a connection to a database. Fortunately, connecting Entity Framework Core to a PostgreSQL database is is very straightforward thanks to Npgsql. The only issue I can think of is converting the DATABASE_URL
Heroku provides to the connection string syntax.