Docker

Docker

Docker

Dec 21, 2017

Building Haskell Apps with Docker

Building Haskell Apps with Docker

Building Haskell Apps with Docker

In this blog post we will show

an example of how we can use Docker to build Haskell applications which we then ship inside

Docker images. We will observe two cases.

First, we will explore the case of developing on the same Linux

distro that we are using for deployment (eg. FROM ubuntu:16.04), and then we will observe the case where we

are not developing on a Linux distro but are still targeting Linux

for deployment. If you are interested to know

about Stack's native

docker support and the stack image container command we will go over that method as well at the end of the post.

Building and deploying on the same OS/Distro

If we are building the Haskell

application on the same Linux distro that we are using for

deployment (in this case a Docker image), then it simplifies things

quite a bit. In this scenario we are able to build our Haskell application locally; using stack just like we would normally do; and

then just embed the compiled binary into the Docker

image. Let's look at the Makefile we will be using, specifically the build target:

 

As we can see we are first building the binary locally using stack build and then invoking docker-compose build that will build the final docker image using docker-compose and the provided Dockerfile. The docker-compose file looks like this:

As we can see from the docker-compose file we are passing in a "build

argument" to the Docker build process. Specifically, we are passing

in the relative location of the app binary located in .stack-work/path/to/myapp. Let's look at the Dockerfile to see how this build argument is used.

In the Dockerfile we are simply copying .stack-work/path/to/myapp into the desired location inside the Dockerfile. NOTE: In this example we have to make to sure to apt-get install any libraries that our application might

require since the binary that we compiled is not statically

linked.

Building on a different OS/Distro

If we are developing on a

machine that has a different OS or distro than that which we are

deploying to, things get a little more complex, since we are trying

to embed dynamically linked libraries into the Docker image.

Specifically, we now have to compile our binary inside a Docker

container rather than on the host machine. Previously this was done done one of 2 ways.

  1. Bundling in all the build time dependencies into the final production docker image

  2. Splitting the build process into Dockerfile.build (that contains all the build time dependencies) and Dockerfile which only has the runtime dependencies and the

    final binary. This was a rather clunky process that required a

    helper shell script that would first build the first Docker image.

    Then it would start a container from that Docker image, fetch the

    compiled binary, start the second container, embed the binary into

    it and finally commit it as an image. You can read more about

    this in the Docker documentation.

Since then, Docker has implemented something called multi stage builds  that simplifies and automates this

process.  The benefit of this is approach is that we don't

bloat our production images with build tools and build

requirements, keeping our image size small.

Docker multi stage builds

In this scenario our Dockerfile looks like this:

In the Dockerfile we first build our app, making use of the fpco/stack-build:lts-9.9

upstream image. After we have built the image, we have another

FROM block that uses the same base image as

fpco/stack-build, where we copy the binary from the previous build.

This allows us to only ship the final binary, without any build

dependencies.

CAVEATS

The main concern with the multi

stage build example above is that we are not re-using any of

Stack's cache capabilities. Instead, we are recompiling the entire

project from scratch every time. This is only an issue for local

development. For CI, we likely want to have a clean slate on every

build anyway.

Using "stack image container"

Stack provides a built in way to

build docker images with our app's executables baked in. While this

support was available before Docker introduced multi staged builds,

it is a bit less flexible compared to the above described methods.

However, it requires far less familiarity with docker and should

therefore be easier to get started with for most people. First let's look at the needed changes to stack.yaml that we copied to stack-native.yaml

As we can see we're instructing

stack to build our executables using docker (which is needed if

you're building on a non-Linux platform like OSX or Windows) and

then we specify some metadata about the container we want stack to

build for us. We specify the base image, the

name of the resulting image and local directories which we have to

add to the resulting image. Stack will add the executable for us

automatically. Let's look at our Dockerfile.base which we will use to build our base image.

As we can see it's not much different from our previous Dockerfile, except for the part about copying the resulting binary since stack will take of that for us. We can build the image using make build-base. And then to build our resulting image we run make build-stack-native. The above make targets look like this:

Now to test our image we can run make run-stack-native which looks like this:

You can read more about stack image container in the Stack documentation.

Conclusion

In this post we've demonstrated

how to build Haskell applications using Docker multi stage builds

to produce Docker images without the unnecessary bloat of build

dependencies. We also shown an alternative approach using stack image container which

produces similarly small docker images but offers a bit less

control. Depending on your project needs you might prefer one method over the other. The code for the above sample application can be found on Github, and contains bits about process management

and permission handling that have been omitted from this post for

brevity.

Suggested Reading

If you liked this post you would also like:

  • How Stack can use Docker under the hood

  • Dockerizing your App

  • Immutability, Docker, and Haskell's ST Type