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?
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
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.)
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
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.
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.