Nix offers declarative dependency management, making it potentially very useful for deploying web apps with exactly the dependencies you need. While this is unnecessary for Heroku-supported languages like Ruby or Node, Haskell deployment to Heroku can benefit greatly from what Nix offers.

In my initial attempt to deploy a Haskell app to Heroku, I came upon Haskell on Heroku. This is perhaps the best method available for deploying Haskell apps to Heroku. Its underlying package manager, Halcyon, even supports installing extra OS packages if needed.

A key component of Haskell on Heroku’s approach is the inclusion of cached Cabal sandboxes, containing many common dependencies for Haskell web apps like Snap and Yesod. In my particular case, this proved not to work too well; the most similar sandboxes still didn’t include a majority of the dependencies for my application. Nonetheless, after a bit of compiling from source, my app was up and running on Heroku.

Although Haskell on Heroku met my immediate needs, it left me wanting something more modular, yet still with cached binaries for efficiency: that is, something like Nix!

One caveat is that Nix is only able to install from its binary caches if they are located in the /nix folder – normally impossible without root access. However, PRoot offers a simple userspace solution to simulate such a directory.

Unfortunately, the need for PRoot means this is mainly useful for prototyping; in production apps, the PRoot jail would add a layer of inefficiency that might be too much to bear. Still, I offer the approach below in the hopes that it may be useful for someone else already using Nix in their Haskell development.

A Nix buildpack

The fruit of my efforts is heroku-buildpack-nix-proot – a Nix buildpack that should only need a default.nix or shell.nix file to operate.

Like Haskell on Heroku, it relies upon Amazon S3 for private storage of cached binaries specific to your codebase. (Presently, it stores a single file containing a closure of the dependencies of your application, although individual cached libraries would also be possible.)

In outline, the approach is this:

  • Initial push
    • download Nix to build cache
    • install Nix using proot
  • Build step (on one-off dyno, or on your own virtual machine)
    • compile app using Nix in BUILD_DIR and store in a closure on S3
  • Final push
    • import Nix closure from S3 and install
    • garbage collect and otherwise pare down the store

Since shared libraries will be located under the simulated /nix directory, you must run your app using a provided script from the buildpack. The README of the heroku-buildpack-nix-proot repo has up-to-date usage instructions.

Not quite there

The above approach works without modification if the app and its dependencies are small. However, a Haskell app using Snap or Yesod results in a slug over 300 MB, too large for deployment. Therefore, we need to do a bit more to get the slug down to size.

I contemplated two alternatives: somehow reduce the files brought in by dependencies, or compile the Haskell app statically. Static compilation presented its own problems, so I endeavored to figure out what could safely be excluded from the slug and still result in a working app.

Solution: Prune the Nix store!

It turns out that Nix retains a lot more than is needed at runtime: namely, the entirety of GCC and GHC are both included in the output. To remedy this, you can create a file called .slugexclude in your repo with patterns to dictate which parts of the Nix store won’t be copied to the slug. (The scripts use rsync, so its syntax applies. See the “INCLUDE/EXCLUDE PATTERN RULES” section of the rsync man page for details.)

A sample .slugexclude looks like this (should be usable out of the box for Haskell):


And with that, my Snap-based app’s slug went from 361.5 MB (over the 300 MB limit) to 145.8 MB. It’s not perfect, but more than adequate for my purposes.

If anyone encounters problems with this buildpack or has suggestions for improvement, I’d be glad to hear them.