One of the attractive features of Go is that it builds your projects as statically linked binaries by default when possible. Which has a very convenient side-effect - it is simple to containerize. On the other hand, there is the most obvious trade-off to it - it is not reusing dependencies installed in the Operating System, but instead bundles everything into single binary.
Unfortunately, traditionally it is not as simple in Haskell as in Go. Here I will present rather portable solution that should be possible to re-use even without Docker infrastructure.
Statically linking blog engine
Here goes the
Dockerfile for my blog project that statically links final
binary that I can then extract and use on any Operating System with the same
cabal configure ...line. This is exactly what makes it possible to build statically linked binary. During the build process, compiler might warn you about some corner cases when statically linking. So, make sure you understand these warnings and risks associated with it.
There are couple of things going on here.
Dockerfile is using multi-stage build system which comes
really handy when what you need is to extract final statically linked binary
without getting the whole GHC/Cabal infrastructure with you. That will also make
final image size small.
Second, it uses one build-time argument,
proxy, that let’s you pass
build-time proxy settings in case you are working behind a corporate proxy. And
strictly speaking this is not necessary at all.
Now, if you will try to build an image, you will notice that it really takes long time to complete:
docker build -t blog . docker build -t blog --build-arg proxy=http://127.0.0.1:3128/ .
“No such protocol name: tcp” error
Sometimes in case your project is using
network package and you are trying to
package it in
alpine image (which is considered to be one of the most slim
images out there for now), you might get the following error in run-time:
Network.BSD.getProtocolByName: does not exist (no such protocol name: tcp) in haskell
This is pretty annoying, especially since you’ve got pretty far getting your
statically linked binary out. In this case, it is possible to solve it by using
ubuntu:17.04 base image with missing dependencies (which I didn’t manage to
find by now in
alpine package repository) - ca-certificates, libgnutls30
Produced image is going to be bigger than the one built on
alpine- but we are talking about
90 MBincrease which can be considered negligible.
Data-files in dependencies
In cases when you are using libraries that have
data-files instruction in its
Cabal-files, you most likely will end up in a situation when your statically
linked binary will fail in runtime. A good example at hands is
Hakyll library that declares among
other things some important data files:
These files are used to render RSS and ATOM feed files on my blog.
The way I solved it is not perfect as it cannot be applied to multiple libraries
at the same time or generically change logic behind
Paths_* module (for that you will likely need to change either GHC or
Cabal). So, to keep it simple I cloned (copied in fact)
into my project structure, upgraded version to
126.96.36.199 and changed the logic
related to getting content for data files in such a way that I try to see if
there are any files with the same path in the current working directory first,
and only then fallback to checking the content with using
Here is how the code looked like before in
And here is how it looks now in
This is not yet a pull request as of now, but I might end up submitting it at some point.
Besides the fact that logic is hidden behind
compilerUnsafeIO, it should be
clear that I first check if there is a local version of a
path and only if it
does not exist locally, I fallback to
It should be possible to make similar changes to other libraries that use
data-filesinstruction. Though I suspect that this size does not necessarily fit all, and there are some other cases when this way is simply not appropriate.
Optimizing for convenient development process
Primary reason being that
cabal update and
cabal install --dependencies-only
will really take long time, and depending on the number of dependencies (direct
and indirect) it might take longer. This can be solved by splitting the process
into two parts:
Part 1, that builds a base image with all the required dependencies.
Part 2, that solely builds your project and packages it as a clean and slim Docker image.
Build new base image with all the dependencies
As a convention, this needs to be built as
blog:devimage. This is the base image name used later.
This will allow you to build a base image once and get back to hopefully smooth development process where you will only need to trigger the build for code changes in your project and not in the base layer.
Build & package final clean and slim image
This will allow you concentrate on producing a final clean and slim image based
alpine:3.7 in this case without wasting too much of your time.
Author Roman Kuznetsov
License Roman Kuznetsov