Skip to content

Instantly share code, notes, and snippets.

@noteed
Last active August 20, 2022 01:20
Show Gist options
  • Save noteed/4155ffad2b1d13ab17ee to your computer and use it in GitHub Desktop.
Save noteed/4155ffad2b1d13ab17ee to your computer and use it in GitHub Desktop.
Notes about creating small Docker images with Haskell binaries.

Notes about creating small Docker images with Haskell binaries.

At first I haven't manage yet to do what I want. In this thread, there is a solution: http://mail.haskell.org/pipermail/haskell-cafe/2015-April/119021.html for the dynamic case. See below for the complete rootfs I use (similar thus to https://github.com/snoyberg/haskell-scratch).

We want a minimal Docker image with the following program.

> cat Example.hs
main = putStrLn "Hello, world."

Compiling the program

Using Halcyon (in a Docker image, but that is not important), we can produce a dynamically-linked binary:

> docker run -v $(pwd):/source noteed/ghc-7-8-4 sh -c \
    "/app/ghc/bin/ghc -O2 -L/app/ghc/usr/lib --make /source/Example.hs"

> $ ldd Example
    linux-vdso.so.1 =>  (0x00007fff5558d000)
    libgmp.so.10 => /usr/lib/x86_64-linux-gnu/libgmp.so.10 (0x00007f39563fb000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f39560f5000)
    librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f3955eec000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f3955ce8000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f3955ad2000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f395570c000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f39554ee000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f3956690000)

We can also produce a statically-linked binary:

> docker run -v $(pwd):/source noteed/ghc-7-8-4 sh -c \
    "apt-get install -q -y libgmp-dev && \
    /app/ghc/bin/ghc -O2 --make \
      -static -optc-static -optl-static -optl-pthread \
      /source/Example.hs"

> ldd Example
  not a dynamic executable

Building the image

In the static case, the rootfs looks simply like this:

> tree rootfs
rootfs
└── Example

0 directories, 1 file

I also tried the approach outlined here for the dynamic case.

> tree rootfs
rootfs
├── Example
├── lib
│   └── x86_64-linux-gnu
│       ├── libc.so.6
│       ├── libdl.so.2
│       ├── libgcc_s.so.1
│       ├── libm.so.6
│       ├── libpthread.so.0
│       └── librt.so.1
├── lib64
│   └── ld-linux-x86-64.so.2
└── usr
    └── lib
        └── x86_64-linux-gnu
            └── libgmp.so.10

6 directories, 10 files

In both cases, building the image is as follow:

> tar -C rootfs -c . | docker import - example

Unfortunately, running

> docker run example /Example

results in an Example process using 100% CPU and doing nothing.

I have tried the static case with the true-asm binary, and it works.

Update: it works

Here is the complete rootfs (for the dynamically compiled Example):

rootfs
├── Example
├── lib
│   └── x86_64-linux-gnu
│       ├── libc.so.6
│       ├── libdl.so.2
│       ├── libgcc_s.so.1
│       ├── libm.so.6
│       ├── libpthread.so.0
│       ├── librt.so.1
│       └── libz.so.1
├── lib64
│   └── ld-linux-x86-64.so.2
└── usr
    └── lib
        └── x86_64-linux-gnu
            ├── gconv
            │   ├── gconv-modules
            │   ├── gconv-modules.cache
            │   ├── UTF-16.so
            │   ├── UTF-32.so
            │   └── UTF-7.so
            └── libgmp.so.10

7 directories, 15 files

And thus:

> tar -C rootfs -c . | docker import - example
cdec64252aa2f9740e45e93890c78c48c1bb4b8765bd77b791755de58574b6c3
> docker run example /Example
Hello, world.

This is quite awesome !

> docker save example > example.tar
> xz example.tar
> du -h example.tar.xz 
1.5M	example.tar.xz

(strip was applied to the binary.)

Chroot

This works with chroot too:

> sudo chroot rootfs /Example

Use qemu to run a kernel + initramfs

You can follow instruction from https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt using our Example as ini t executable (add threadDelay to ensure that our init doesn't exit):

> mv rootfs/Example rootfs/init
> cd rootfs ; find . | cpio -o -H newc | gzip > ../rootfs.cpio.gz ; cd ..

> qemu-system-x86_64 \
    -nographic \
    -kernel /boot/vmlinuz-3.19.0-7-generic \
    -initrd rootfs.cpio.gz \
    -append "console=ttyS0" \
    /dev/zero

-nographic and console=ttyS0 are needed only to get qemu output in the current terminal.

Use C-a h for help and C-a x to exit qemu.

@dkubb
Copy link

dkubb commented Jul 11, 2015

@noteed What about using something like https://github.com/nilcons/ghc-musl to compile the statically-linked binary?

@noteed
Copy link
Author

noteed commented Jul 15, 2015

@dkubb Yes, that's something I have to try. And also via Nix.

@dkubb
Copy link

dkubb commented Jul 16, 2015

@noteed FYI I was able to get it working properly. I made a simple script that makes it pretty easy to create statically linked haskell binaries: https://gist.github.com/dkubb/842da22c212c944673b1#file-build-hs

@noteed
Copy link
Author

noteed commented Jan 20, 2017

For the Nix case, the final export as a tarball here seems what I want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment