Nix for Haskell: Static Builds

Lobsters Hottest Tools

Summary

This tutorial explains how to create statically-linked executables for Haskell projects using Nix, covering configuration of GHC for static builds and integration with Docker.

<p><a href="https://lobste.rs/s/medvuo/nix_for_haskell_static_builds">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 06/18/26, 02:03 PM

# Nix for Haskell: Static Builds Source: [https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/) In the[previous post](https://abhinavsarkar.net/posts/nix-for-haskell/), we learned how to get started with managing and building a[Haskell](https://haskell.org/)project with[Nix](https://nixos.org/)\. In this post, we learn how to easily create statically\-linked executables for Haskell projects with Nix\. In the[previous post](https://abhinavsarkar.net/posts/nix-for-haskell/), we learned how to get started with managing and building a[Haskell](https://haskell.org/)project with[Nix](https://nixos.org/)\. In this post, we learn how to easily create statically\-linked executables for Haskell projects with Nix\. This post is a part of the series:**Nix for Haskell**\. 1. [Getting Started](https://abhinavsarkar.net/posts/nix-for-haskell/) 2. **Static Builds**👈 ### Contents 1. [Static Builds](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#static-builds) 2. [Enabling Static Builds in GHC](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#enabling-static-builds-in-ghc) 3. [Configuring the Application](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#configuring-the-application) 4. [Rooting Static Build Dependencies](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#rooting-static-build-dependencies) 5. [Finale](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#finale) 6. [Bonus: Building a Docker Image](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#bonus-building-a-docker-image) 7. [Conclusion](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#conclusion) I recommend going through the[previous post](https://abhinavsarkar.net/posts/nix-for-haskell/), because we are going to start off from where we left last time \(ignoring the bonus sections\)\. This is how our project’s directory tree looks at this point: ![](https://abhinavsarkar.net/images/nix-for-haskell-static-builds/dir-tree.svg) `Main\.hs`is the default generated main file that prints “Hello, Haskell\!”\.`ftr\.cabal`is the default generated[Cabal](https://www.haskell.org/cabal/)file\.`sources\.\(json\|nix\)`are generated by[Niv](https://github.com/nmattia/niv)to pin[Nixpkgs](https://github.com/NixOS/nixpkgs/)to a particular revision\.[`nixpkgs\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#nixpkgs_nix)provides the nixpkgs that we use for building tools and dependencies\.[`package\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#package_nix)and[`shell\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#shell_nix)build the package and manage the Nix shell respectively\. We are not going to touch any of these files in this post\. Let’s get started\. ## Static Builds[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#static-builds) A[static build](https://en.wikipedia.org/wiki/static_build)is an executable that is statically\-linked against all the libraries it depends on\. This is in contrast to a dynamically\-linked executable, which contains references to the libraries it depends on, and those libraries are loaded and linked when the executable runs\. While dynamic linking has its[benefits](https://en.wikipedia.org/wiki/Static_build#Dynamic_linking), the main advantage of static linking is that the executable can be shipped by itself, without needing to ship or install dependency libraries\. This makes it quite attractive for deploying backend services\. You download and deploy that one binary executable file and you are done\! No need to care about installing and maintaining its dependencies\. Many compilers support static builds—[Go](https://golang.org/)and[Rust](https://www.rust-lang.org/)being two with great ease\. Haskell compiler[GHC](https://www.haskell.org/ghc/)also supports it, but not out\-of\-the\-box\. To statically link a Haskell executable, we need to configure GHC itself, and then configure the executable build as well\. We also need to configure GHC to link with[musl](https://en.wikipedia.org/wiki/musl)libc\. That’s where Nix helps us by smoothing out the process[1](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn1)\. ## Enabling Static Builds in GHC[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#enabling-static-builds-in-ghc) As mentioned, first we need a GHC configured to do static builds\. We create a nixpkgs derivation, separate from[`nixpkgs\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#nixpkgs_nix), that contains the custom configured GHC\. ``` { arch, ghcVersion ? (import ./nixpkgs.nix { }).haskellPackages.ghc.version, }: let getGHCWithVersion = lib: "ghc" + lib.replaceStrings [ "." ] [ "" ] ghcVersion; sources = import ./sources.nix; in import sources.nixpkgs { system = arch + "-linux"; overlays = [ ( final: prev: let compiler = getGHCWithVersion prev.lib; prevHPackages = prev.haskell.packages.${compiler}; in prev.lib.attrsets.recursiveUpdate prev { haskell.packages.${compiler} = prevHPackages.override { ghc = prevHPackages.ghc.override { enableRelocatedStaticLibs = true; enableShared = false; enableDwarf = false; enableProfiledLibs = false; enableDocs = false; enableNativeBignum = true; }; buildHaskellPackages = prevHPackages.buildHaskellPackages.override (old: { ghc = final.haskell.packages.${compiler}.ghc; buildHaskellPackages = final.haskell.packages.${compiler}; }); }; } ) ( final: prev: let compiler = getGHCWithVersion prev.lib; in { haskellPackages = prev.haskell.packages.${compiler}; ghc = prev.haskell.packages.${compiler}.ghc; } ) (final: prev: { haskell = prev.haskell // { packageOverrides = prev.lib.composeExtensions prev.haskell.packageOverrides ( hfinal: hprev: { mkDerivation = args: hprev.mkDerivation ( args // { doCheck = false; doHaddock = false; enableLibraryProfiling = false; enableExecutableProfiling = false; } ); } ); }; }) ]; config = { }; } ``` nix/nixpkgs\-static\-ghc\.nixLet’s go over it piece\-by\-piece\. First, we take the`arch`and`ghcVersion`parameters, letting us build the package for different architectures \([X86\-64](https://en.wikipedia.org/wiki/X86-64)and[AArch64](https://en.wikipedia.org/wiki/AArch64)\), and for different GHC versions\. We default the`ghcVersion`to the default GHC in nixpkgs\. The derivation is same as[`nixpkgs\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#nixpkgs_nix), except we add some overlays\. The first overlay adds the custom configured GHC for static builds\. We enable certain configurations for that purpose: `enableRelocatedStaticLibs = true`Configures GHC runtime system and core packages to be built with position independent code so that they can be loaded for template Haskell\.`enableShared = false`Disables building dynamically\-linkable libraries, so they are built only as static archives\.`enableDwarf = false`Disables[DWARF](https://en.wikipedia.org/wiki/DWARF)\-based stack traces, because it is unavailable on musl targets\.`enableProfiledLibs = false`Disables building profiling enabled libraries\.`enableDocs = false`Disables generation of documentation\.`enableNativeBignum = true`Makes GHC use pure\-Haskell based native bignum backend[`ghc\-bignum`](https://hackage.haskell.org/package/ghc-bignum)instead of[GMP](https://gmplib.org/), so that the package executables it creates are GPL\-free\. You may remove this setting if you are okay with GPL executables\.The`buildHaskellPackages`related lines set the custom GHC as the compiler for Haskell\-based tools used in Nix[2](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn2)\. The second overlay makes[`cabal2nix`](https://github.com/NixOS/cabal2nix/)—the tool used to convert`\.cabal`files into Nix derivations—use the custom GHC\. The third overlay disables documentation generation, testing and profiling of all Haskell libraries built with the custom GHC\. We do this to save the build time, assuming that static builds are for release only, and the docs, tests and profiling are done using a normal GHC\. Building this custom GHC may take anywhere from several minutes to several hours depending on the build machine configuration[3](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn3)[4](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn4)\. But this is a one\-time price to pay, as long as we keep the GHC build around\. Next, we configure our package to be built as a statically\-linked executable\. ## Configuring the Application[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#configuring-the-application) The`package\-static\.nix`file is equivalent of the[`package\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#package_nix)file from the previous post, but builds statically\-linked exes\. ``` { arch, ghcVersion ? (import ./nix/nixpkgs.nix { }).haskellPackages.ghc.version, }: let pkgsOrig = import ./nix/nixpkgs-static-ghc.nix { inherit arch ghcVersion; }; pkgs = pkgsOrig.pkgsMusl; hlib = pkgs.haskell.lib.compose; staticDeps = import ./nix/static-deps.nix { inherit pkgs; }; inherit (staticDeps) libffi zlib numactl; in pkgs.lib.pipe (pkgs.haskellPackages.callCabal2nix "ftr" (pkgs.lib.cleanSource ./.) { }) [ hlib.dontHaddock hlib.dontHyperlinkSource hlib.dontCoverage hlib.disableExecutableProfiling hlib.disableLibraryProfiling hlib.disableSharedLibraries hlib.justStaticExecutables hlib.enableDeadCodeElimination (hlib.overrideCabal (old: { enableParallelBuilding = true; buildTools = (old.buildTools or [ ]) ++ [ pkgsOrig.buildPackages.lld ]; })) (hlib.appendConfigureFlags [ "-O2" "--ghc-option=-fPIC" "--ghc-option=-optl=-static" "--ghc-option=-split-sections" "--ghc-option=-optl-fuse-ld=lld" "--ld-option=-fuse-ld=lld" "--with-ld=ld.lld" "--ld-option=-Wl,--gc-sections,--build-id,--icf=all" "--extra-lib-dirs=${libffi}/lib" "--extra-lib-dirs=${zlib}/lib" "--extra-lib-dirs=${numactl}/lib" ]) ( src: pkgs.stdenv.mkDerivation { name = "${src.name}-compressed"; inherit src; nativeBuildInputs = [ pkgsOrig.upx ]; installPhase = '' mkdir -p $out cp -R $src/. $out chmod -R +w $out/bin upx -q --lzma -1 $out/bin/* chmod -R -w $out/bin ''; }) ] ``` package\-static\.nixLet’s go over the file in parts\. `package\-static\.nix`also takes`arch`and`ghcVersion`as parameters, and passes them to`nix/nixpkgs\-static\-ghc\.nix`to create the nixpkgs with the custom GHC as described above\. This give us`pkgsOrig`, from which we get the`pkgsMusl`version\.`pkgsMusl`is same nixpkgs, except every executable in it links to musl libc\. We capture this as`pkgs`, and use it to build our Haskell package\. When linking the executable, we need to link it against static version of all the dependency libraries it depends on\. That’s what`nix/static\-deps\.nix`file provides us\. We’ll look at it in the next section, but for now, we see that it gives us the`libffi`,`zlib`, and`numactl`libraries[5](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn5)\. Finally, we get to the package configuration\. It starts the same as[`package\.nix`](https://abhinavsarkar.net/posts/nix-for-haskell/#package_nix), using`cabal2nix`to connect the Haskell project to Nix, but then, we provide a list of custom configurations\. We disable[Haddock](http://www.haskell.org/haddock/)docs, hyperlinked source docs, coverage tests, profiling, and shared library build\. We enable static executable build and dead code elimination\. Then we configure cabal to run builds with multithreading, and add[`lld`](https://web.archive.org/web/20260618/https://lld.llvm.org/)to its list of build tools\. Then, we add many configuration flags: `\-O2`Enables optimization\.`\-\-ghc\-option=\-fPIC`Enables emission of position independent code\.`\-\-ghc\-option=\-optl=\-static`Enables static linking\.`\-\-ghc\-option=\-split\-sections`Enables[split sections](https://downloads.haskell.org/ghc/latest/docs/users_guide/phases.html#ghc-flag-fsplit-sections)to produce smaller binaries\.`\-\-ghc\-option=\-optl\-fuse\-ld=lld`,`\-\-ld\-option=\-fuse\-ld=lld`,`\-\-with\-ld=ld\.lld`Configures GHC to use[`lld`](https://web.archive.org/web/20260618/https://lld.llvm.org/)as the linker, which is much faster than the default linker\. You can omit these lines to use the default linker\. Or you can replace all metions of`lld`with`mold`to use the[Mold](https://github.com/rui314/mold)linker, which may be even faster depending on your project\.`\-\-ld\-option=\-Wl,\-\-gc\-sections,\-\-build\-id,\-\-icf=all`Enables reductions in binary size by removing dead code\.`\-\-extra\-lib\-dirs=\.\.\.`These lines allow GHC to link the output executable against the static version of the mentioned dependency libraries\.Finally, the last function in the pipeline uses[UPX](https://upx.github.io/)to compress the output executable\. This generally results in a large reduction in the binary size[6](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn6)\. Now we can actually build the statically\-linked exe: ``` $ nix-build --argstr arch x86_64 package-static.nix $ nix-build --argstr arch aarch64 package-static.nix $ nix-build --argstr arch x86_64 --argstr ghcVersion "9.12" package-static.nix ``` The first and second line above build the exe for the X86\-64 and AArch64 architectures with the default GHC version\. The third line specifies a different GHC version to build with\. Here is the cleaned\-up output log for the first command: Output log``` $ nix-build --argstr arch x86_64 package-static.nix these 2 derivations will be built: /nix/store/42a291bq7ydvkdy3fdsyj82axrfsi6sy-ftr-0.1.0.0.drv /nix/store/2c2l50la8291q0jrqlc23bybaxwip8y2-ftr-0.1.0.0-compressed.drv building '/nix/store/42a291bq7ydvkdy3fdsyj82axrfsi6sy-ftr-0.1.0.0.drv' on 'ssh-ng://builder@linux-builder'... copying 1 paths... copying path '/nix/store/l9ls307kzxby72hqj4yl7ri7m8s3b3fk-source' to 'ssh-ng://builder@linux-builder'... building '/nix/store/42a291bq7ydvkdy3fdsyj82axrfsi6sy-ftr-0.1.0.0.drv'... Running phase: setupCompilerEnvironmentPhase Build with /nix/store/717lxds14ra0ndbnin2qhdhh91d3b69g-ghc-musl-native-bignum-9.10.3. Running phase: unpackPhase unpacking source archive /nix/store/l9ls307kzxby72hqj4yl7ri7m8s3b3fk-source source root is source Running phase: patchPhase Running phase: compileBuildDriverPhase setupCompileFlags: -package-db=/nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/tmp.EPOEKNjT6Z/setup-package.conf.d -threaded [1 of 2] Compiling Main ( /nix/store/4mdp8nhyfddh7bllbi7xszz7k9955n79-Setup.hs, /nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/tmp.EPOEKNjT6Z/Main.o ) [2 of 2] Linking Setup Running phase: updateAutotoolsGnuConfigScriptsPhase Running phase: configurePhase configureFlags: --verbose --prefix=/nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0 --libdir=$prefix/lib/$compiler/lib --libsubdir=$abi/$libname --with-gcc=gcc --package-db=/nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/tmp.EPOEKNjT6Z/package.conf.d --ghc-option=-j4 --ghc-option=+RTS --ghc-option=-A64M --ghc-option=-RTS --disable-library-profiling --disable-profiling --disable-shared --disable-coverage --enable-static --disable-executable-dynamic --disable-tests --disable-benchmarks --enable-library-vanilla --disable-library-for-ghci --enable-split-sections --enable-library-stripping --enable-executable-stripping -O2 --ghc-option=-fPIC --ghc-option=-split-sections --ghc-option=-optl-fuse-ld=lld --ld-option=-fuse-ld=lld --ld-option=-Wl,--gc-sections,--build-id,--icf=all --with-ld=ld.lld --ghc-option=-optl=-static --extra-lib-dirs=/nix/store/yi771fg1dfj1bg618vv5flmisy8zw3hm-libffi-3.5.2/lib --extra-lib-dirs=/nix/store/jk77s356gjn68dcrzpz1m7m5amzxmkw8-zlib-1.3.2-static/lib --extra-lib-dirs=/nix/store/044b10glmg0f3yyijmrwrgv5lsys6x6n-numactl-2.0.18/lib --extra-lib-dirs=/nix/store/5gq3bzba6wxj84gqvqjb7bk6i1h7wjbp-ncurses-6.6/lib --extra-lib-dirs=/nix/store/m1j2f9b1h6pbq1mq5ibnw4cpp60w5dfi-libffi-3.5.2/lib --extra-include-dirs=/nix/store/j9c1ifaa7vph3zxfbzb55y1frm0vp4xm-musl-iconv-1.2.5/include --extra-lib-dirs=/nix/store/lrrmafbkrpa4f3wxfz6a3sd3dv6xgp7n-numactl-2.0.18/lib [snip] Running phase: buildPhase Preprocessing executable 'ftr' for ftr-0.1.0.0... Building executable 'ftr' for ftr-0.1.0.0... [1 of 1] Compiling Main ( app/Main.hs, dist/build/ftr/ftr-tmp/Main.o ) [2 of 2] Linking dist/build/ftr/ftr Running phase: haddockPhase Running phase: installPhase Installing executable ftr in /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0/bin Warning: The directory /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0/bin is not in the system search path. Running phase: fixupPhase shrinking RPATHs of ELF executables and libraries in /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0 shrinking /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0/bin/ftr patchelf: cannot find section '.dynamic'. The input file is most likely statically linked checking for references to /nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/ in /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0... patchelf: cannot find section '.dynamic'. The input file is most likely statically linked patching script interpreter paths in /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0 stripping (with command strip and flags -S -p) in /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0/bin copying 1 paths... copying path '/nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0' from 'ssh-ng://builder@linux-builder'... building '/nix/store/2c2l50la8291q0jrqlc23bybaxwip8y2-ftr-0.1.0.0-compressed.drv' on 'ssh-ng://builder@linux-builder'... copying 0 paths... building '/nix/store/2c2l50la8291q0jrqlc23bybaxwip8y2-ftr-0.1.0.0-compressed.drv'... Running phase: unpackPhase unpacking source archive /nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0 source root is ftr-0.1.0.0 Running phase: patchPhase Running phase: updateAutotoolsGnuConfigScriptsPhase Running phase: configurePhase no configure script, doing nothing Running phase: buildPhase no Makefile or custom buildPhase, doing nothing Running phase: installPhase Ultimate Packer for eXecutables Copyright (C) 1996 - 2026 UPX 5.1.1 Markus Oberhumer, Laszlo Molnar & John Reiser Mar 5th 2026 File size Ratio Format Name -------------------- ------ ----------- ----------- 1356096 -> 525804 38.77% linux/amd64 ftr bin/ftr [linux/amd64, LZMA/1] Packed 1 file. Running phase: fixupPhase shrinking RPATHs of ELF executables and libraries in /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed shrinking /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed/bin/ftr patchelf: no section headers. The input file is probably a statically linked, self-decompressing binary checking for references to /nix/var/nix/b/19b7g2frvcvani3cnj61lsb4fq/b/ in /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed... patchelf: no section headers. The input file is probably a statically linked, self-decompressing binary patching script interpreter paths in /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed stripping (with command strip and flags -S -p) in /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed/bin copying 1 paths... copying path '/nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed' from 'ssh-ng://builder@linux-builder'... /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed ``` The output log mentions: > patchelf: cannot find section ‘\.dynamic’\. The input file is most likely statically linked But we can verify for ourselves: ``` $ file /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed/bin/ftr /nix/store/j8gg1x3vrlb5dc1mh149ys0nih9fvmwk-ftr-0.1.0.0-compressed/bin/ftr: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), BuildID[sha1]=df53df80d301b4bba2a7634a4169c6291d64ea72, statically linked, no section header ``` There is one last thing to take care of\. ## Rooting Static Build Dependencies[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#rooting-static-build-dependencies) Dynamically\-linked Haskell builds contain references to their dependency libraries and GHC that was used to build it\. If you use[`direnv`](https://direnv.net/)or install a dynamically\-linked executable, it creates Nix GC roots for the libraries and GHC, preventing them from being garbage\-collected by Nix\. But statically\-linked builds have no references to anything, as intended\. So we need to create GC roots by ourselves to the libraries and the GHC toolchain\. This is even more important because building the custom GHC may be a very time\-consuming affair\. First we list all dependencies in a separate file: ``` { pkgs }: { ghc = pkgs.haskellPackages.ghc; jailbreak-cabal = pkgs.haskellPackages.buildHaskellPackages.jailbreak-cabal; cabal2nix-unwrapped = pkgs.cabal2nix-unwrapped; gmp6 = pkgs.gmp6.override { withStatic = true; }; libffi = pkgs.libffi.overrideAttrs (old: { dontDisableStatic = true; }); ncurses = pkgs.ncurses.override { enableStatic = true; }; zlib = pkgs.zlib.static; numactl = pkgs.numactl.overrideAttrs (oldAttrs: { configureFlags = (oldAttrs.configureFlags or [ ]) ++ [ "--enable-static" "--disable-shared" ]; }); } ``` nix/static\-deps\.nixThis file lists the dependency libraries and the GHC toolchain\. Notice how we override each library’s config to make it statically\-linkable\. I’ve included some additional libraries here \(`gmp6`and`ncurses`\) that are generally used by Haskell projects, but we don’t use them in this project\. You may have to add more of such libraries depending on your project’s dependencies\. We already saw how we use this file in`package\-static\.nix`\. Now, we use it to create Nix GC roots: ``` { arch, ghcVersion ? (import ./nix/nixpkgs.nix { }).haskellPackages.ghc.version, }: let pkgsOrig = import ./nix/nixpkgs-static-ghc.nix { inherit arch ghcVersion; }; pkgs = pkgsOrig.pkgsMusl; in pkgs.symlinkJoin { name = "static-deps"; paths = builtins.attrValues (import ./nix/static-deps.nix { inherit pkgs; }); } ``` package\-static\-deps\.nix`package\-static\-deps\.nix`simply gathers all dependencies from`nix/static\-deps\.nix`and creates a directory with symlinks to them\. This brings us to the finale\. ## Finale[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#finale) We create a bash script`build\-static\.sh`that builds the statically\-linked executable, and creates Nix GC roots for all dependencies and the toolchain: ``` #!/usr/bin/env bash set -euo pipefail nix-build --argstr arch "$1" package-static.nix nix-store --add-root ".gcroots/static-deps-$1" \ --realise $(nix-instantiate --argstr arch "$1" --quiet package-static-deps.nix) \ > /dev/null ``` build\-static\.shThe root is created at`\.gcroots/static\-deps\-x86\_64`for X86\-64 architecture, for example\. You can use[`nix\-tree`](https://github.com/utdemir/nix-tree)to explore it\. This concludes our short tutorial on how to build statically\-linked executables for Haskell projects with Nix\. ## Bonus: Building a Docker Image[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#bonus-building-a-docker-image) One more thing static builds are great for: wrapping them into[Docker](https://www.docker.com/)images\. Since they are much smaller than dynamically\-linked executables and their dependencies combined, they are better to package as Docker images\. Here’s how we do it: ``` { arch }: let pkgs = import ./nix/nixpkgs.nix { system = arch + "-linux"; }; ftr = import ./package-static.nix { inherit arch; }; in pkgs.dockerTools.buildLayeredImage { name = "ftr"; tag = "latest"; contents = [ pkgs.dockerTools.caCertificates ftr ]; extraCommands = '' mkdir -p var/lib/ftr var/cache/ftr etc/ftr echo 'ftr:x:1000:1000::/var/lib/ftr:/sbin/nologin' > etc/passwd echo 'ftr:x:1000:' > etc/group ''; fakeRootCommands = '' chown -R 1000:1000 var/lib/ftr var/cache/ftr ''; enableFakechroot = true; config = { User = "1000:1000"; Cmd = [ "/bin/ftr" ]; Volumes = { "/var/lib/ftr" = { }; "/var/cache/ftr" = { }; "/etc/ftr" = { }; }; }; } ``` docker\.nixThis image also shows how to package extra Nix packages in images, setting up a non\-root user to run the executable, and setting up user\-owned directories to expose as volumes\. We can build the image by running: ``` $ nix-build --argstr arch x86_64 docker.nix [snip] /nix/store/cdghjrgdcf7vm0jbgjl5556zx3j4zjj1-ftr.tar.gz ``` Then we can load and run the image on Docker like so: ``` $ docker load -i /nix/store/cdghjrgdcf7vm0jbgjl5556zx3j4zjj1-ftr.tar.gz ``` ## Conclusion[\#](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#conclusion) This post shows how to configure GHC and Haskell projects to build statically\-linked executables that are fully portable and independent\. If your Haskell project has any complex requirements, such as custom dependency versions, patched dependencies, custom non\-Haskell dependencies etc\., this setup may not scale\. In such case you can either grow this setup by learning Nix in more depth with the help of the official[Haskell with Nix docs](https://wiki.nixos.org/w/index.php?title=Haskell)and this[great tutorial](https://github.com/Gabriella439/haskell-nix), or switch to using a framework like[haskell\.nix](https://input-output-hk.github.io/haskell.nix/)or[haskell\-flake](https://github.com/srid/haskell-flake)\. For dealing with complex static builds,[static\-haskell\-nix](https://github.com/nh2/static-haskell-nix/)project may be of help\. If you have any questions or comments, please leave a comment below\. If you liked this post, please share it\. Thanks for reading\! --- 1. I have tested this setup for GHC 9\.10\+ and X86\-64 and AArch64 architectures only\.[↩︎](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fnref1) 2. Without this config, Nix will build a separate GHC for building Haskell\-based tools used in Nix\.[↩︎](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fnref2) 3. You may need a remote Nix Linux builder to build GHC and your package if you are not on Linux or not on the right architecture\. You may set up a[remote builder](https://nix.dev/manual/nix/latest/advanced-topics/distributed-builds.html)or[Linux builder on macOS](https://nixos.org/manual/nixpkgs/stable/#sec-darwin-builder)\.[↩︎](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fnref3) 4. Building GHC is memory intensive\. You may require few GBs of RAM\.[↩︎](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fnref4) 5. Some of your project’s dependency libraries may link to[GMP](https://gmplib.org/)directly\. In such cases, the libraries provide Cabal flags to remove GMP dependency\. If you don’t want GMP linked to your executable, you’ll need to override the Nix derivation for such libraries to pass those Cabal flags\.[↩︎](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fnref5) 6. Note that we get the tools`lld`and`upx`from`pkgsOrig`, the original nixpkgs, not the musl one\. We don’t need musl version of these tools for them to work, and doing that would simply cause our builds to take longer\.[↩︎](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fnref6) This post is a part of the series:**Nix for Haskell**\. 1. [Getting Started](https://abhinavsarkar.net/posts/nix-for-haskell/) 2. **Static Builds**👈 ### Like, repost, or comment - [Lobsters](https://lobste.rs/s/medvuo) - [Comments below](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#comment-container) ### Send a Webmention for this post Posted by at[https://abhinavsarkar\.net/posts/nix\-for\-haskell\-static\-builds/](https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/) ### Like this post? Subscribe to get future posts by email\.

Similar Articles

nix-build in under 100 lines

Lobsters Hottest

The article demystifies the Nix build process by reimplementing nix-build in under 100 lines of Go, showing that turning a derivation into a store path is essentially an exec.

Development shells with Nix: four quick examples

Michael Stapelberg

A tutorial demonstrating four ways to set up development shells using Nix, including interactive one-offs, config files, and hermetic Nix Flakes, using GoCV and OpenCV as an example.

Nix needs relocatable binaries

Lobsters Hottest

The article identifies the problem that Nix binaries are not relocatable, causing hash changes and recompilation when the store prefix changes, and proposes using relative paths with $ORIGIN in RUNPATH to achieve relocatability without invalidating caches.

The postmodern build system

Lobsters Hottest

A blog post exploring the design of an ideal 'postmodern' build system that prioritizes trustworthy incremental builds, maximized computation reuse, and distributed builds, using Nix as a reference point.

Introduction to nixidy - Kubernetes GitOps with nix

Lobsters Hottest

nixidy is a Nix-based tool for managing Kubernetes GitOps deployments that replaces Helm value files and Kustomize overlays with typed, reproducible Nix expressions. This tutorial walks through setting up a nixidy project with Argo CD, generating plain YAML for review.