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:

`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\.