Using Nix to deploy a Haskell web app to Heroku
Posted on 16 February, 2020
Let's say we've written a web app in Haskell.
$ curl http://localhost:8000/hello/World
Hello, World!
$ curl http://localhost:8000/hello/Haskell
Hello, Haskell!
It sure would be nice if we could share it with other people. Let's deploy this wonderful thing to the internet using Heroku.
Haskell and Nix
Thanks to the Nix using Haskell community packaging up Haskell libraries and executables is very convenient.
First, we'll make a directory for our project.
~ $ mkdir haskell-on-heroku
~ $ cd $_
~/haskell-on-heroku $
Then we can ask Cabal to get us started with a scaffold.
~/haskell-on-heroku $ nix run \
> nixpkgs.cabal-install \
> --command cabal init \
> --minimal \
> --cabal-version=2.4
Finally, we add a default.nix
which makes use of cabal2nix
.
{ haskellPackages ? (import <nixpkgs> {}).haskellPackages }:
haskellPackages"haskell-on-heroku" ./. {} .callCabal2nix
And it's now ready to try out.
~/haskell-on-heroku $ nix run \
> --file default.nix \
> env --command cabal new-run
# ...
Hello, Haskell!
An amazing web application
Our web server uses the nifty Haskell library Servant server so we'll need to add it and Warp as dependancies to haskell-on-heroku.cabal
.
executable haskell-on-heroku
main-is: Main.hs
build-depends: base >=4.12 && <4.13+ , servant-server
+ , warp
default-language: Haskell2010
We'll probably also need to put the implementation of our app in Main.hs
.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall #-}
module Main where
import Control.Applicative ((<|>))
import Data.Proxy (Proxy (Proxy))
import Network.Wai.Handler.Warp (run)
import Servant
:>),
( (Capture,
Get,
Handler,
PlainText,
serve,
)import System.Environment (getEnv)
type Greeter =
"hello"
:> Capture "name" String
:> Get '[PlainText] String
greet :: String -> Handler String
=
greet name pure $ "Hello, " <> name <> "!"
getPort :: IO Int
=
getPort read <$> getEnv "PORT" <|> pure 8000
main :: IO ()
= do
main <- getPort
port $ serve (Proxy @Greeter) greet run port
A deployable image
The Docker-using members of the Nix community have made building docker-compatible images from Nix derivations very convenient.
To try it out put the following in a file somewhere, say ~/hello.nix
.
let
nixpkgs = import <nixpkgs> {};
in
{
nixpkgs.dockerTools.buildImage name = "hello-docker-nix";
tag = "latest";
contents = [ nixpkgs.hello ];
}
Then build it.
~ $ nix-build hello.nix
Load the result.
~ $ docker load < result
And finally run it.
~ $ docker run hello-docker-nix:latest hello
Hello, world!
We can put our package in a docker-compatible image in a similar fashion. Put the following in a file called release.nix
in your haskell-on-heroku
project directory.
{ nixpkgs ? import <nixpkgs> {} }:
let
inherit (nixpkgs)
callPackage
dockerTools;
package = callPackage ./. {};
in
{
dockerTools.buildImage name = "haskell-on-heroku";
tag = "latest";
contents = [
package];
}
Using the callPackage
helper means we're less likely to accidentally end up with multiple versions of the Nix packages set. If that should happen, our app would still run fine, we just might end up with a bigger than necessary image.
Let's try to build it and see how big it is.
~/haskell-on-heroku $ nix-build release.nix
# ... build output
~/haskell-on-heroku $ docker load < result
3f5a871dd9ee: Loading layer 38.1MB/38.1MB
Loaded image: haskell-on-heroku:latest
It's about 38.1 megabytes. That's not bad.
Now, I'm going to save us some trouble and add busybox
to our image and also make sure it has a Cmd
configured.
diff --git a/release.nix b/release.nix
index 8fa2527..e14a534 100644--- a/release.nix
+++ b/release.nix
@@ -3,7 +3,8 @@
let
inherit (nixpkgs)
callPackage- dockerTools;
+ dockerTools
+ busybox;
package = callPackage ./. {};
in@@ -12,5 +13,9 @@ in
tag = "latest";
contents = [
package+ busybox
];+ config = {
+ Cmd = ["/bin/${package.pname}"];
+ };
}
We need to specify a command so Heroku knows how to run the container it'll create from our image. We need busybox
because Heroku will attempt to run our image's command with bash -c
. Images built with Nix's dockerTools
are so minimal they don't have bash
or even /bin/sh
.
Shipping it
We now have everything we need. The last steps are to create a Heroku application, push our image to the Heroku registry and release it.
First, assuming you already have a Heroku account, log into the Heroku command line.
~/haskell-on-heroku $ nix run nixpkgs.heroku \
> --command heroku login
Then also login into the container registry.
~/haskell-on-heroku $ nix run nixpkgs.heroku \
> --command heroku container:login
Next create an app.
~/haskell-on-heroku $ nix run nixpkgs.heroku \
> --command heroku create
Build our image.
~/haskell-on-heroku $ nix-build release.nix
Load it.
~/haskell-on-heroku $ docker load < result
Tag it with our Heroku application's name and the process type we'd like to run it as.
~/haskell-on-heroku $ docker tag \
> haskell-on-heroku \
> registry.heroku.com/infinite-anchorage-09330/web
Push it to the registry.
~/haskell-on-heroku $ docker push \
> registry.heroku.com/infinite-anchorage-09330/web
And, lastly, release it.
~/haskell-on-heroku $ nix run nixpkgs.heroku \
> heroku container:release -a infinite-anchorage-09330 web
Hey presto, our amazing application will now be accessible by the whole world. How cool.
That does seem like a lot to remember, however, so we'd best pop as much of it as we can in a script. A script we might call ./deploy
.
#!/usr/bin/env bash
set -exu
app_name=$1
result=$(nix-build --no-out-link release.nix)
docker load < $result
docker tag haskell-on-heroku registry.heroku.com/$app_name/web
docker push registry.heroku.com/$app_name/web
nix run nixpkgs.heroku --command \
-a $app_name web heroku container:release
With that building and deployment is a little more manageable.
~/haskell-on-heroku $ ./deploy infinite-anchorage-09330
I've prepared a repository of everything we've gone through here as a reference. With any luck it may help someone get their awesome Haskell application onto the mad expanse that is the internet.