Releasing a Haskell Web App on Heroku with Docker

Using Heroku to serve a Servant App

Releasing Haskell web applications on Heroku has become much easier with Heroku’s Docker support. This article explains how to deploy a Servant application on Heroku using Docker.

I’ve prepared an example application you can use to try deploying to Heroku. This article is divided into three sections. The first section explains how to run the example application locally. The second section explains how to run the example application locally using Docker. The third section explains how to deploy this application to Heroku.

If you want to deploy to Heroku without running locally first, feel free to skim through the first and second sections. However, if you’re new to Haskell development, I recommend going through all three sections.

Running the application locally WITHOUT Docker

The example application is a small JSON API. It provides two APIs. One is to submit simple comments. The other is to display all comments that have been submitted. The comments are saved to a PostgreSQL database.

The following will walk through how to build and run the application locally, without involving Docker or Heroku.

An error may occur when building the application because of missing PostgreSQL libraries.

On Arch Linux, these missing PostgreSQL libraries can be installed with the following command:

$ sudo pacman -Ss postgresql-libs

On Ubuntu, the following command can be used:

$ sudo apt-get install libpq-dev

Other platforms may use a different command to install these libraries.

Once the required PostgreSQL libraries have been installed, try running stack build again. It should succeed this time.

Now try running the application:

$ stack exec -- servant-on-heroku-api

Oops! It should fail with the following error:

servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
Is the server running on host "localhost" (::1) and accepting
TCP/IP connections on port 5432?
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP/IP connections on port 5432?
)

The example application is trying to connect to PostgreSQL. Comments are stored in PostgreSQL, so you need PostgreSQL running locally.

Setup PostgreSQL

Most OSs and distributions will have different ways of installing PostgreSQL. Check with your platform documentation on how to install PostgreSQL.

For example, here is the Arch Linux documentation for installing PostgreSQL. Here is the Ubuntu documentation.

Once PostgreSQL is installed and running, try running the application again:

$ stack exec -- servant-on-heroku-api

Another oops! It should fail with the following error:

servant-on-heroku-api: libpq: failed (FATAL: role "mydbuser" does not exist
)

Looks like a PostgreSQL user and database need to be setup for our application. If you check out the application source code (src/Lib.hs), you can see that it is reading in the DATABASE_URL environment variable and using it to connect to the PostgreSQL server.

If the DATABASE_URL environment variable is not specified, the application defaults to using:

It is trying to use the user mydbuser with password mydbpass to access the database named mydb. Let’s create this user and database in PostgreSQL. The following commands are specific to Arch Linux. They may differ slightly if you are on a different platform. Check your platform documentation if they don’t seem to be working.

Let’s try running the servant-on-heroku image. This will run the application in Docker:

$ docker run --interactive --tty --rm servant-on-heroku

Oh no! It looks like the PostgreSQL problem is back:

servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
Is the server running on host "localhost" (::1) and accepting
TCP/IP connections on port 5432?
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP/IP connections on port 5432?
)

What’s happening here? Well, since the servant-on-heroku container is running as a Docker container, by default it can’t see our local network. It can’t see that PostgreSQL is running on localhost:5432.

Here’s a small trick we can use. When running the servant-on-heroku container, we can tell Docker to just let the container use our local network interface. That way, it can see PostgreSQL:

Get the Application Running on Heroku

In order to get the application actually running on Heroku, the following command is used:

$ heroku container:push web

This builds a Docker image for the application based on the Dockerfile in the current directory. Internally, docker build is used to do this. If the image was already built in the previous step (when running docker build from the command line), then this heroku container:push command will just use the previously built image. The image is sent to Docker’s Container Registry.

Setting up the PostgreSQL database creates a configuration variable called DATABASE_URL. Heroku passes this configuration variable to the application on startup as an environment variable. As discussed in a previous section, the application uses DATABASE_URL to connect to the correct database3.

Heroku’s DATABASE_URL can also be used to connect to the database on the command line:

Future (Normal) Releases

Performing future releases of the application is extremely easy. Just run the following command:

$ heroku container:push web

This rebuilds the docker image for the application and pushes it to Heroku’s container repository. It then restarts the dynos so they are running with the new code for the application.

Future Work

This application works pretty well, but there are a couple places for improvements. The lowest hanging fruit would probably be the Dockerfile. Here are a couple ideas that would make the Dockerfile a little better:

Use a slimmer image as the base image for the Dockerfile. Right now it is using Heroku’s images, but I don’t think there is any reason that something like Alpine Linux couldn’t be used.

Base the image on something with stack, GHC, and popular Haskell libraries already installed. This would greatly reduce the time it takes to do the very initial docker build.

At the very end of the Dockerfile, remove stack, GHC, and all Haskell libraries. This would hopefully make the docker image a little smaller. It would take less bandwidth to send the image to Heroku’s container repository.

It would also be nice to use something like docker-compose to setup the PostgreSQL database using Docker when running locally.

Alternatives to Docker for Deploying on Heroku

The only strong alternative to using Docker for deploying Haskell code to Heroku is haskellonheroku. This is a normal Heroku buildpack for Haskell. With this buildpack, you are able to use Heroku like you would with dynamic languages. All you need to do is git push your code to Heroku’s remote git repository. The new code is automatically compiled and deployed.

This sounds really good in theory, but in pracitce haskellonheroku has two big drawbacks:

Heroku build times are limited to 15 minutes. haskellonheroku gets around this in a complicated way, requiring use of Amazon S3 to upload prebuilt libraries before doing a git push.

haskellonheroku uses halcyon internally to accomplish most build steps. halcyon is a tool similar to stack and nix. However, it appears that development has stopped 2 years ago. halcyon does not support any of the latest GHC versions.

halcyon might have been nice a few years ago before stack existed. But now that stack is regularly used for Haskell development, moving to an alternative build tool doesn’t seem like a good decision.4

Related Work

Conclusion

As long as you have Docker running on your local machine, it’s pretty easy to get your Haskell code on Heroku. Heroku’s free plan is nice for testing application ideas and showing them to others. It may not work for any sort of business application, but as a proof-of-concept, it’s great!

If you decide your proof-of-concept works well and you want to release it, it’s easy to add a credit card to Heroku and start running on their cheapest paid tier. Heroku has a very nice upgrade path.

Footnotes

These seven steps are slightly complicated. Ideally, it should be possible to install GHC, install all the application dependencies, and build the application in just one command. However, I have separated it into multiple commands to take advantage of Docker’s caching ability. When re-running docker build, only commands where the input has changed will be re-run.

For example, if you change the servant-on-heroku.cabal file and re-run docker build, it will rebuild the image from (4), starting with installing dependencies from the application’s .cabal file. docker build does not have to re-run (1), (2), or (3). It uses cached versions of the image.

This means that if all you change is the application source code under src/ and re-run docker build, all docker build has to do is re-run (5), (6), and (7). It doesn’t have to install GHC or the application’s Haskell dependencies. This reduces a large part of the build-time. Future builds will take just a few minutes, instead of one hour.↩

There are multiple kinds of dynos. However, it’s not something that we need to worry about for our simple web API.↩

Heroku also makes use of the PORT environment variable for telling your application which port to listen on.↩