Docker image size

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.

Does anybody know any solution?

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 :laughing:

2 Likes

Agreed that Nix solves this, disagree that using Nix is a downside =). There are some dockerTools examples at GitHub - bellroy/wai-handler-hal-example: Example repo for wai-handler-hal

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.

6 Likes

Do you have some examples of this? I see @jackdk linked GitHub - bellroy/wai-handler-hal-example: Example repo for wai-handler-hal but I don’t understand how this is using dockerTools.

GitHub shows “languages” for a repository. Click on Nix then you can easily find files like wai-handler-hal-cdk/runtime/tiny-container.nix.

1 Like

Thanks for the reply. I used GHC-9.2.7 as the resolver but it should of course have been lts-20.24.

This is a complete flake.nix using dockerTools

{
  # 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

1 Like

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.