Deploying Yesod with Haskell
I finally deployed my Yesod 184.108.40.206 based yesod-oauth-demo project to Heroku, but it was not the smooth ride I anticipated.
In this post I want to show what code changes where necessary and what I had to do on my machine and on the Heroku side to get the first deployment done. It's a longer post and not meant to be a rant. If you find errors or want to improve this post, feel free to send me a pull request.
TL;DR: Get a virtual machine that has the same library versions as your Heroku server to build your Yesod application. You have to live with the fact that you need to upload large binaries to Heroku, use a throw-away git branch for that.
Catch the qualified import
Each Yesod scaffold generated site includes a Heroku Procfile that explains the basic steps for the Heroku deployment with commented lines.
The steps are simple:
- copy and adjust the
deploy/directory to your Yesod root directory
- create a
package.jsonfile in your Yesod root directory to fake a node.js application for Heroku
- add the
herokupackage dependency to your cabal file and adjust your code
- build your Yesod project and deploy it to Heroku
The first two steps where straight forward, but I had to make some (in retrospective)
to the sample code to get the
heroku package running.
If you are using a Yesod newer release than 220.127.116.11, you have a
that you don't have to adjust the code from the Procfile.
loadHerokuConfig functions are
depending on types from the mysterious
AT module. It's a qualified
import of the
Data.Aeson.Types module. If you have not done so, cabal
will tell you that you also need to include the
aeson package in your
I had to do a bit more research to find out that
M.union is a reference to the
Data.HashMap.Strict module, which adds a cabal dependency for the
Shared libraries and old Ubuntu versions
The deployment guide from the Yesod wiki, helped me a lot with the following shared library problem:
As you might know, Haskell is a compiled language, which is a good thing. The bad thing about this is that you have to deploy a compiled binary to Heroku. This is also not bad per se, but it gets ugly when your system has different library versions than Heroku. My first deployment attempt failed because the standard C library (libc) had the wrong version on my system.
The reason for this version mismatch is that I'm using Ubuntu 12.04, while Heroku is relying on 10.04. Back in 2010 libc 2.10 was part of the Ubuntu distribution, but Ubuntu 12.04 comes with libc 2.15. I solved this by using a Ubuntu 10.04 "Lucid Lynx" VirtualBox image with Vagrant to compile the binary.
Vagrant to the rescue
Vagrant is a smart command line wrapper for VirtualBox. The setup is simple:
$ gem install vagrant $ vagrant box add lucid64 http://files.vagrantup.com/lucid64.box $ vagrant init lucid64 $ vagrant up
Having my fresh Ubuntu 10.04 box, I used
vagrant ssh to get into the
system. There was the next impediment: only ghc6 is supported for this
old Ubuntu version. After rolling my eyes for quite some time, I decided
to install the current
If you want to do this, I strongly recommend that you increase the memory of your Vagrant virtual machine. The whole process is described in detail on the Haskell Platform for Linux site.
To build the executable with the same libc version, make sure that you do the following steps in your virtual Ubuntu:
$ cd /vagrant $ cabal install [...] $ cabal clean && cabal configure && cabal build [...] Linking dist/build/oauth/oauth ...
After you got your binary, you can start deploying or testing.
Test your build locally
After my first failed deployment to Heroku, I wanted to test the build locally from within my virtual Ubuntu. First I configured Vagrant to forward port 8080 from my host system to the VM.
You need to tell your Yesod application what database to use with the
DATABASE_URL environment variable (Heroku will do the same). In my
case I choose to use the PostgreSQL database from my host system, which is
reachable from within the virtual Ubuntu via 10.0.2.2.
$ cd /vagrant $ export DATABASE_URL="postgres://[USERNAME]:[PASSWORD]@10.0.2.2:1363/[DATABASE]" $ ./dist/build/oauth/oauth Production -p 8080
Yesod will only print output messages if things go wrong. In all other cases you can start requesting on port 8080.
These steps are only necessary when you want to deploy to Heroku the first time and do not differ from deploying non-haskell applications.
- Login to Heroku:
- Upload you SSH public key:
heroku keys:add ~/.ssh/id_rsa.pub
- Create a new Heroku app:
heroku apps:create --stack cedar yesod-oauth-demo
- Add a new git-remote:
git remote add heroku email@example.com:yesod-oauth-demo.git
It's important that you use the cedar stack, because it allows the execution of arbitrary binaries.
Always check for missing shared libraries
It may happen that your Yesod binary depends on shared libraries, that are not available on Heroku. You have to check this for each deployment. If you don't want to risk downtime, you can do this before you deploy your new release to Heroku.
After checking locally with
ldd ./dist/build/oauth/oauth for shared
library dependencies on your system (or VM), you have to check if all
dependencies are also available on Heroku. At the moment I'm using
heroku run bash to get a shell and search them by hand.
If you found libraries that are not available on Heroku, create a
libs/ directory on you local system/VM, copy the missing libraries to
that folder and commit + push them with the build Yesod binary to Heroku.
You need to tell Heroku to use that directory by executing
heroku config:add LD_LIBRARY_PATH=./libs
the first time that you want to deploy custom binaries. Don't forget to
also add you binary, as described in the next step.
Once you went through the initial trouble of setting up your deployment machine and adjusting the code, all deployments are straight forward:
- Create a (throw away) git branch
git checkout -b deploy
- Create a binary using
cabal clean && cabal configure && cabal build
- Add the binary, commit and push to Heroku master
The reason for the throw away git branch is that Heroku expects you to push the code using a git branch. The lack of direct Haskell/Yesod support on Heroku results in committing and pushing a binary, because Heroku will not compile the code for you. You could use your master git branch to commit those binaries and push them to Heroku, but this will result in a large git branch, because each deployment will add a new large blob to your history.
Remember that you need to build your code in your VM, to have the same shared library versions as Heroku.
$ vagrant up $ vagrant ssh # cd /vagrant # git checkout -b deploy # cabal clean && cabal configure && cabal build # git add -f ./dist/build/oauth/oauth # git commit -m "binary" # git push -f heroku deploy:master # git branch -d deploy
I need to use
git add -f because I added the
dist/ folder to my
I hope that this was helpful. If you have feedback or want to improve this post, feel free to use pull requests or write an email.