I am working on a docker image and I am using the official haskell:9.2.7-slim-buster image.
I have it currently setup so stack installs another version of ghc, but that makes the docker image very big. I would like to keep using the version given by the image. I have found that setting system-ghc: true will make so it doesn’t download any new version. But that makes it so that the dependencies won’t download.
What resolver are you using? Be sure that it is a resolver that uses the version of GHC installed. The last LTS for GHC 9.2.7 is lts-20.24. If you would like to use newer versions of packages than are available in that LTS, you can try configuring them as extra-deps, perhaps setting allower-newer: true, but that does not always work. To use newer dependencies more easily, you could use Cabal instead of Stack.
Tangentially related to your question: I have found that dockertools on nix generate really small images for haskell programs. The down side is that you need to use nix
I believe the native Docker solution to this problem is to do a multi-stage build and copy the executable and any libraries it needs from the build container to the final image.
{
# inspired by: https://serokell.io/blog/practical-nix-flakes#packaging-existing-applications
description = "A Hello World in Haskell with a dependency and a devShell";
inputs = {
nixpkgs.url = "nixpkgs";
};
outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
nixpkgsFor = forAllSystems (system: import nixpkgs {
inherit system;
overlays = [ (self.overlay system) ];
});
in
{
overlay = (system: final: prev: rec {
foobar = final.haskell.packages.ghc924.callPackage (import ./default.nix) {};
foobar-clean = final.haskell.lib.justStaticExecutables foobar;
foobar-docker = final.dockerTools.buildImage {
name = "foobar";
tag = "latest";
copyToRoot = final.buildEnv {
name = "foobar-root";
paths = [ foobar-clean nixpkgsFor.${system}.cacert ];
pathsToLink = [ "/bin" "/etc" "/share" ];
};
config = {
Cmd = [ "/bin/foobar-exe" ];
Env = [
"FOOBAR_NODE_URL"
"FOOBAR_PG_HOST"
"FOOBAR_PG_USER"
"FOOBAR_PG_DATABASE"
"FOOBAR_PG_PASSWORD"
"FOOBAR_PG_PORT"
"GHCRTS"
];
};
};
});
packages = forAllSystems (system: {
foobar = nixpkgsFor.${system}.foobar;
foobar-clean = nixpkgsFor.${system}.foobar-clean;
foobar-docker = nixpkgsFor.${system}.foobar-docker;
});
defaultPackage = forAllSystems (system: self.packages.${system}.foobar-clean);
checks = self.packages;
devShell = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
haskellPackages = pkgs.haskell.packages.ghc924;
in
haskellPackages.shellFor {
packages = p: [self.packages.${system}.foobar];
withHoogle = true;
buildInputs = with haskellPackages; [
haskell-language-server
cabal-install
pkgs.zlib
];
# Change the prompt to show that you are in a devShell
# shellHook = "export PS1='\\e[1;34mdev > \\e[0m'";
});
};
}
Check the definition of foobar-docker. I ran nix build .#foobar-docker on the repository directory and it would give me a result symlink to a docker tarball. I loaded that tarbal with docker load <result
i can confirm that multi-stage builds work find for making small docker images based on Haskell projects - we used to use them and had 8MB docker images for our app’s deployment. All the second stage needs is a barebones image (I think we used to build the app in alpine, then use alpine as the base of the second stage), an any C libraries your app needs; we needed to install gmp and libpq for Postgres, an that was about it.