forever getline

@jhickner's blog

Easy Haskell App Deployment With Git and Upstart (or Systemd)

At work I run a bunch of small apps that do little analytics and monitoring tasks, all spread across servers at Rackspace and Amazon. Haskell is great for these types of apps since the resident memory usage is often only 2-4mb, which makes good use of low-ram servers.

This post describes a simple deployment setup that lets me run unmodified haskell programs as daemons and have them automatically re-compiled and re-launched when I do a git push.

Git hooks and Upstart/systemd make this a snap. At the end of this post we’ll have the standard daemon commands like stop, start and restart, automatic rolling log files and even automatic respawning if our program crashes, all for the price of a tiny config file.

We’ll use a remote monitoring app called intrepid that I’ve been working on as an example, and I’ll walk through the steps to set up easy deployment.

Step 1: Set up a remote Git repo

Assuming you have a local copy of your app’s source stored in git and complete with a proper .cabal file (easy to create with cabal init), the next step is to set up a receiving repo you can push to. We’ll make it --bare, because it’s actually simpler to do a checkout from a bare repo after every push and then run that than it is to try to use the repo’s working directory.

On your server, create a folder for the repo and initialize it:

1
2
3
mkdir -p ~/projects/Intrepid/repo
cd ~/projects/Intrepid/repo
git --bare init

and create the directory we’ll check out to:

1
mkdir ~/projects/Intrepid/bin

We’ll push to this repo in a minute, but first let’s do some setup. In the bare repo there’s a hooks directory. You can place scripts here that will be run after or before various actions are performed on the repo (more on hooks here).

We’ll set up a post-receive hook that will run after the repo recieves a push. Our hook will do a checkout, build our app and restart it. Create a file called post-receive and place it in the repo’s hooks directory. Don’t forget to chod +x so it’s runnable.

post-receive
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh

# check out to our working folder by setting GIT_WORK_TREE
GIT_WORK_TREE=/home/apps/projects/Intrepid/bin git checkout -f

# cd there, build the app
cd /home/apps/projects/Intrepid/bin
cabal configure
cabal install --only-dependencies
cabal build

# restart with upstart
restart intrepid

All set! This script checks out our code, builds it and restarts after every push. Next we’ll set up Upstart, which will provide that restart line at the bottom.

Step 2: Setting up Upstart/systemd

Upstart and systemd are two popular replacements for the venerable SysV init, the program we’ve been using to stop and start unix daemons for over 150 years (approximately). Depending on your distro, you probably have one or the other installed already. The setup for each is very similar.

Here’s an example Upstart config file for intrepid:

intrepid.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
description "Intrepid - remote monitoring tool"

# start after the filesystem and ethernet are up
start on (local-filesystems and net-device-up IFACE=eth0)

stop on shutdown

# restart automatically if we crash
respawn

# tell upstart how to start our app
script
  cd /home/apps/projects/Intrepid
  dist/build/intrepid/intrepid
end script

Your conf file should be named <appname>.conf and placed in /etc/init. That’s it! We get a lot for those few lines, but there’s a lot more Upstart can do (docs).

And here’s an example systemd service file (courtesy of vagif on reddit), which should be placed in /etc/systemd/system.

intrepid.service
1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Intrepid remote monitoring tool
After=network.target

[Service]
User=apps
WorkingDirectory=/home/apps/projects/Intrepid/bin
ExecStart=/home/apps/projects/Intrepid/bin/dist/build/intrepid/intrepid
Restart=always

[Install]
Alias=intrepid.service
WantedBy=multi-user.target

Step 3: Push

Finally, in our local repo we’ll add the remote repo as a push target:

1
git remote add apps ssh://apps@appserver/~/projects/Intrepid/repo

and push to it:

1
git push apps master

If all goes well, you should see your app compiling successfully, followed by a message like this from upstart, telling you your app is running:

1
intrepid start/running, process 5037

And we’re done! You now have all the standard daemon commands, so you can ssh to your server and stop/start/restart/status intrepid if you have Upstart or systemctl start/stop/restart/status intrepid on systemd. If it crashes, it will be automatically restarted. Upstart will place log files at /var/log/upstart/<appname>.log.

And of course, every time you git push, your running app will be automatically updated!

Comments