Haskell and Docker

Installing a Haskell build stack can be daunting, so building and/or shipping programs with docker seems like a good way to shield users from this complexity. I’d like to learn more about how people use these (or whether everybody is using nix instead).

Useful resources that I found so far:

The background is that we are currently working on making official pandoc images a reality. We opted for Alpine images in order to keep image size low.

Any tips on how to combine docker, Haskell, and its build tools effectively would be most welcome.

1 Like

whether everybody is using nix instead

People think nix is not easy to learn, which might be true, but it is actually quite simple to use it for Haskell projects. If your project has a cabal file, you only need a file called default.nix containing:

(import <nixpkgs> { }).haskellPackages.developPackage { 
  root = ./.; 
}

Then nix-shell --run 'cabal new-build' to build the project or nix-shell --run 'cabal new-run' to run it. The nix-shell command will put you in an environment containing the right GHC version and with the package dependencies.

EDIT: Here is the current default.nix for building pandoc on Linux and Mac via nix.

2 Likes

This may be off topic but speaking of using pandoc as an user here’s my default.nix that gets me pandoc along with the latex suite of packages:

with import <nixpkgs> { };
runCommand "dummy" {
  buildInputs = [ pandoc texlive.combined.scheme-full ];
} ""

At work i use a build-container with a dockerfile like:

FROM ubuntu
RUN apt-get update
RUN apt-get install -y g++ gcc libc6-dev libffi-dev libgmp-dev make xz-utils zlib1g-dev git gnupg curl 
libexpat-dev locales && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN locale-gen de_DE.UTF-8 && update-locale
RUN curl -sSL https://get.haskellstack.org/ | sh
ENV resolver=lts-13.12
ENV LC_ALL de_DE.utf8
RUN mkdir -p /usr/src/build
RUN cd /usr/src/build && /usr/local/bin/stack setup --resolver ${resolver}
#preinstall some needed packages so they get cached
RUN cd /usr/src/build && /usr/local/bin/stack install aeson               --resolver ${resolver} --work-dir=.stack-work
RUN cd /usr/src/build && /usr/local/bin/stack install async               --resolver ${resolver} --work-dir=.stack-work
[...]
# copy project & build
COPY . /usr/src/build/
RUN cd /usr/src/build && /usr/local/bin/stack install --work-dir=.stack-work

and then copy the binary from containername:~/.local/bin to .
Then i build an image just containing the binary & all deps for that

FROM ubuntu
RUN apt-get update && apt-get install -y netbase libc6-dev libffi-dev libgmp-dev xz-utils zlib1g-dev gnupg curl libexpat1 locales && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN locale-gen de_DE.UTF-8 && update-locale
RUN mkdir -p /app
ENV LC_ALL de_DE.utf8
COPY $BINARYNAME /app
COPY data /app/data
WORKDIR /app
ENTRYPOINT ["/app/$BINARY"]

this yields an ~150mb-image. ~50mb is base-ubuntu, ~50mb got added via apt as dependencies & ~50mb is the binary that was produced.

The build-image gets quite big (~4GB), but that is just for building. You could achieve more with using alpine as base (but some deps are not in their repos), strip the binary, optimize for size, etc.

Some other inspiration:

https://futtetennismo.me/posts/docker/2017-11-24-docker-haskell-executables.html

2 Likes

I set up a docker development environment for the moot project. You can check out my pull request (now merged) to see the specifics here:

Feel free to ask any questions you have about it!

Nix can create docker image, I don’t know how it do it but when I look into the result it seem not based on any linux distribution.

For example, try building following default.nix (via nix-build)

# create docker images using nix-build
# load with `docker load < result`
# run with `docker -it` for interactive with vt

{ pkgs ? import <nixpkgs> {} }:
with pkgs;

dockerTools.buildImage {
  name = "mydocker";
  tag = "latest";

  contents = [
    pandoc
  ];
}

and I got 417M (compressed)

[wizzup@ nix-docker]$ nix-build && du -sh $(readlink -f result)
/nix/store/x5bnq4b220b616pzzrdh3wsz1f5xssm9-docker-image-mydocker.tar.gz
417M    /nix/store/x5bnq4b220b616pzzrdh3wsz1f5xssm9-docker-image-mydocker.tar.gz

while pandoc itself is 121M

[wizzup@ nix-docker]$ nix-build '<nixpkgs>' -A pandoc && du -sh $(readlink -f result)
/nix/store/kinrcadh0h8p77kxyhpyzx4gja0s7v8r-pandoc-2.7.1
121M    /nix/store/kinrcadh0h8p77kxyhpyzx4gja0s7v8r-pandoc-2.7.1
1 Like

I’ve recently added support to optionally split the executables in a separate output. In Nix, they are compiled statically by default, while library is compiled dynamically. This should get the docker images to a few 10 MBs.

Unfortunately, it seems like Paths libddir reference stays in the Nix closure, so it’s not much help here until we figure out why splitting sections didn’t get rid of it.

But you can use https://alexandre.peyroux.io/posts/2019-04-06-nix-and-haskell.html to compile even system libraries statically with musl, now your Docker image should be just a couple of MB.

Also about packing development tools into docker.

I am experimenting with stack to the point that it is somewhat usable but still can’t get the stack update integrated in the image. When I run stack new test it will always pull the update.

Help/suggestions are always welcome, the first goal is ability to build hello world project in mounted volume.

$ docker run -v "$PWD/data":/data haskell_stack stack build