Minimal deployment image?

Hi, in my quest for a small deployment image for a Haskell web app, I fell into the rabbit hole of static linking.
Long story short, it’s complicated (no TH, certain libraries make GHC flake out with missing linker symbols, etc.)

Which made me wonder: perhaps static linking is a non-goal when you rely on Docker for deployment anyway. It’s more needed for CLI tools I guess.

So, suggestions wanted: how do you keep your Haskell deployment images to a minimum size?

  • multi-stage Docker builds on top of Alpine: what directories need to be copied from the build stage to the final stage?
  • “distro-less” images. Haskell is not supported yet, have you considered it?
  • please don’t suggest Nix as I cannot make heads or tails of it
8 Likes
  • please don’t suggest Nix as I cannot make heads or tails of it

Or if anyone really wants to suggest Nix and has achieved a relatively minimal container setup with it, please provide a complete example that us non-Nixers can build and tinker with :smiley:

3 Likes

Here you go: Ryan Hendrickson / nix-haskell-hello-world-web · GitLab

I haven’t done the work to make the closure as small as it could be; the container image is currently 69 MB. Some things in the dependency graph can probably be clipped out. But it’s pretty minimal in terms of amount of Nix code. Doesn’t use flakes or anything like that.

To try it out, clone the repo, and run:

nix-build container.nix
docker load -i result
docker run -p 8000:8000 --rm nix-haskell-hello-world-web
# server running on http://localhost:8000 until you Ctrl-C

(This assumes that you have a basic Nix setup using channels. Edit the pkgs = ... line in container.nix if you would like to pin to a particular version of Nixpkgs.)

2 Likes

There’s probably a lot here that is out of date, but Haskell Web Server in a 5MB Docker Image gives an example of how this problem was approached in 2015.

2 Likes

Ah yes, should have checked there first. FPCO build (and blogged about!) a lot of good stuff.

The example repo for wai-handler-hal (a package for connecting wai webapps to AWS Lambda) has one that statically links the binary, then UPX-compresses it: wai-handler-hal-example/tiny-container.nix at 328f798d1ca56284e3d7c1fef4ce47102ccaf1fa · bellroy/wai-handler-hal-example · GitHub

Because it’s designed for AWS Lambda, it also includes their “runtime interface emulator” to run Lambda Functions locally, as well as a shell script to launch things and a copy of Busybox to run it. It needs flakes enabled, but builds and loads into a 13.6MB image:

$ nix build .#tiny-container
$ ./result | docker load
$ docker image ls | grep wai-handler
wai-handler-hal-example-tiny-container latest 79ab7b6dd4e9 55 years ago 13.6MB
4 Likes

FWIW, I’ve had a good experience deploying CLI apps by just building on Alpine. Example GitHub - chrisdone-archive/cron-daemon: Run a program as a daemon This one has a dockerfile and building instructions in the README. I do a similar thing for Hell, which does use TH.

GHC has its own allocator so it’s unaffected, but the C libraries might be a bit slower than glibc. Some libs might just not be available on alpine, happened to me.

I think you’ll be already happy deploying server apps as containers, but just wanted to share that it’s not too difficult to fully static link. I’d agree for servers it’s a non goal.

1 Like

I feel a little bad posting a second time in the thread that was not supposed to be about Nix solutions (sorry OP), but I got my Nix solution down to 7.88 MB, still with no external dependencies other than Nixpkgs, no UPX¹, and only a small amount of inscrutable Nix magic. Same repo, same instructions; just use tiny-container.nix instead of container.nix, and, uh, be prepared to wait for most of the toolchain to get built from scratch because none of this is in the Nix caches.


¹ I consider UPX to be Goodharting—it's 2025, every mainstream OS has transparent filesystem compression available for data at rest, and any server worth half a cent will gzip data over the wire. There may be edge cases that benefit from bundling self-extracting code with your application, but any apparent size reduction likely only replaces the gains you'd get from turning on FS compression, which you should already be considering if you're short on space.
4 Likes

… case in point, after figuring out the dynamic dependencies of your app with ldd, you can copy them into the serving image like so:

COPY --from=builder /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
COPY --from=builder /usr/lib/libz.so.1 /usr/lib/libz.so.1
COPY --from=builder /usr/lib/libgmp.so.10 /usr/lib/libgmp.so.10

(here starting from one of the dynamically linked GHC bindists based on Alpine e.g. https://downloads.haskell.org/~ghc/9.6.6/ghc-9.6.6-x86_64-alpine3_12-linux.tar.xz )

You eventually get a Docker image under 100 MB even for a large project, which I call a net win ^^

1 Like