Search code examples
javadockergradletestcontainersnix

Unable to build Spring Boot Application with Testcontainers using Nix due to Docker build time dependency


I can't get a basic Spring Boot Application with Testcontainers being built using Nix, because of a build time dependency on a working docker environment. I put my efforts into my GitHub in order to preserve my learnings from now on.

My goal

  • Using Java 17
  • Spring Boot
  • Custom JRE (as small as possible)
  • Testcontainers for integration testing with JUnit
  • aarch64 Docker image being built

My setup

  • MacBook Pro 16" 2021; M1 Max; 32 GB RAM
  • macOS Sonoma 14.4
  • nix-darwin Setup
  • linux-builder enabled:
linux-builder = {
    enable = true;
    ephemeral = true;
    maxJobs = 4;
    config = {
      nix.settings.sandbox = false; # cannot get it working in a pure fashion
      networking = {
        nameservers = [ "8.8.8.8" "1.1.1.1" ];
      };
      virtualisation = {
        darwin-builder = {
          diskSize = 40 * 1024;
          memorySize = 8 * 1024;
        };
        docker = {
          enable = true;
          rootless = {
            enable = true;
            setSocketVariable = true;
          };
        };
        cores = 6;
      };
    };
  };

Building

My Flake looks like that:

{
  description = "Inventory Backend Flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
        };
        version = "0.0.1-SNAPSHOT";
        inventory-jre = pkgs.stdenv.mkDerivation {
          name = "inventory-jre";
          buildInputs = [ pkgs.openjdk17 ];
          src = self;
          buildPhase = ''
            jlink --add-modules java.base,java.xml --output custom-jre
          '';
          installPhase = ''
            mkdir -p $out
            cp -r custom-jre/* $out/
            chmod +x $out/bin/*
          '';
        };
        application = pkgs.stdenv.mkDerivation {
          # disabling sandbox
          __noChroot = true;
          name = "inventory-backend";
          src = self;
          version = version;
          buildInputs = [ pkgs.openjdk17 ];

          buildPhase = ''
            export GRADLE_USER_HOME=$(mktemp -d)
            chmod +x ./gradlew
            ./gradlew clean build --info
          '';

          installPhase = ''
            mkdir -p $out
            cp -r build/libs/inventory-backend-${version}.jar $out/
          '';
        };

        dockerImage = pkgs.dockerTools.buildImage {
          name = "inventory-backend";
          tag = "latest";
          created = builtins.substring 0 8 self.lastModifiedDate;
          copyToRoot = [application inventory-jre];

          config = {
            Cmd = [ "${inventory-jre}/bin/java" "-jar" "${application}/inventory-${version}.jar" ];
            ExposedPorts = {
              "8080/tcp" = {};
            };
            Volumes = {
              "/tmp" = {};
            };
          };
        };
      in {
        devShells.default = pkgs.mkShell {
          buildInputs = [ pkgs.openjdk17 ];
        };

        packages.default = application;

        packages.dockerImage = dockerImage;

        defaultPackage = self.packages.default;

      }
    );
}

In order to build the docker image, I'm running nix build -vvv .#packages.aarch64-linux.dockerImage --print-out-paths, because I want to create a docker image to be run on aarch64-linux docker.

Error

Trying to build the image as described above, it actually fails, because Testcontainers wouldn't find a valid Docker environment when building for aarch64.

Even adding a working docker environment to my linux-builder did not do the trick.

Error log:

 2024-03-18T14:39:38.826Z ERROR 3789 --- [    Test worker] o.t.d.DockerClientProviderStrategy       : Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
    As no valid configuration was found, execution cannot continue.
    See https://www.testcontainers.org/on_failure.html for more details.
    2024-03-18T14:39:39.831Z ERROR 3789 --- [    Test worker] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Exception during pool initialization.

    java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
        at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:256)
        at java.base/java.util.Optional.orElseThrow(Optional.java:403)
        at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:247)
        at org.testcontainers.DockerClientFactory.getOrInitializeStrategy(DockerClientFactory.java:150)
        at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:186)
        at org.testcontainers.DockerClientFactory$1.getDockerClient(DockerClientFactory.java:104)
        at com.github.dockerjava.api.DockerClientDelegate.authConfig(DockerClientDelegate.java:108)
        at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:321)
        at org.testcontainers.jdbc.ContainerDatabaseDriver.connect(ContainerDatabaseDriver.java:134)
        at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138)
        at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:359)
        at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:201)
        at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:470)
        at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561)
        at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:100)
        at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112)

What I know

  • Testcontainers spins up docker containers when running the JUnit tests of my project.
  • skipping tests is no viable option
  • ditching Testcontainers is not a viable option at the moment
  • Testcontainers looks for DOCKER_HOST

My linux-builder has a valid DOCKER_HOST and a working docker setup:

[builder@nixos:~]$ echo $DOCKER_HOST
unix:///run/user/1000/docker.sock

However in the log it says WARN 3789 --- [ Test worker] o.t.d.DockerClientProviderStrategy : DOCKER_HOST unix:///var/run/docker.sock is not listening.

So I suppose, it uses a separate environment.


How do I set up a docker build dependency in Nix(OS) correctly?


Solution

  • Nix Flakes do provide the possibility to add tests by putting derivations in the checks attribute and running them as nix flake check. Therefore I skip Gradle tests in the build process and instead run them as checks, which seems the more correct way to fix that issue.

    It's probably not perfect yet (I would appreciate contribution), but my Flake looks like that now:

    {
      description = "Inventory Backend Flake";
    
      inputs = {
        nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
        flake-utils.url = "github:numtide/flake-utils";
      };
    
      outputs = { self, nixpkgs, flake-utils, ... }:
        flake-utils.lib.eachDefaultSystem (system:
          let
            pkgs = import nixpkgs {
              inherit system;
            };
            version = "0.0.1-SNAPSHOT";
            inventory-jre = pkgs.stdenv.mkDerivation {
              name = "inventory-jre";
              buildInputs = [ pkgs.openjdk17 ];
              src = self;
              buildPhase = ''
                jlink --add-modules java.base,java.xml --output custom-jre
              '';
              installPhase = ''
                mkdir -p $out
                cp -r custom-jre/* $out/
                chmod +x $out/bin/*
              '';
            };
            applicationSource = pkgs.stdenv.mkDerivation {
              name = "inventory-backend-src";
              src = self;
              version = version;
              installPhase = ''
                mkdir -p $out
                cp -r ./* $out/
              '';
            };
            application = pkgs.stdenv.mkDerivation {
              # disabling sandbox
              __noChroot = true;
              name = "inventory-backend";
              version = version;
              buildInputs = [ pkgs.openjdk17 ];
    
              buildPhase = ''
                export GRADLE_USER_HOME=$(mktemp -d)
                chmod +x ./gradlew
                ./gradlew clean build --info
              '';
    
              installPhase = ''
                mkdir -p $out
                cp -r build/libs/inventory-backend-${version}.jar $out/
              '';
            };
    
            dockerImage = pkgs.dockerTools.buildImage {
              name = "inventory-backend";
              tag = "latest";
              created = builtins.substring 0 8 self.lastModifiedDate;
              copyToRoot = [application inventory-jre];
    
              config = {
                Cmd = [ "${inventory-jre}/bin/java" "-jar" "${application}/inventory-${version}.jar" ];
                ExposedPorts = {
                  "8080/tcp" = {};
                };
                Volumes = {
                  "/tmp" = {};
                };
              };
            };
          in {
            devShells.default = pkgs.mkShell {
              buildInputs = [ pkgs.openjdk17 ];
            };
    
            packages.default = application;
    
            packages.dockerImage = dockerImage;
    
            checks.gradletests = pkgs.testers.runNixOSTest {
              name = "Gradle Test: Inventory Backend Stub";
    
              nodes = {
                machine1 = { pkgs, ... }: {
                  environment.systemPackages = [pkgs.openjdk17 applicationSource];
                  nix.settings.sandbox = false;
                  virtualisation.docker.enable = true;
    
                  virtualisation.memorySize = 2 * 1024;
                  virtualisation.msize = 128 * 1024;
                  virtualisation.cores = 2;
               };
             };
    
             testScript = ''
               machine1.wait_for_unit("network-online.target")
               machine1.execute("cp -r ${applicationSource}/* ${applicationSource}/.* .")
               machine1.execute("java -version")
               machine1.succeed("./gradlew test --no-daemon --debug");
             '';
           };
          }
        );
    }
    
    

    Close to the bottom you find the test definition in the attribute checks.gradletests. It essentially just runs ./gradlew test --no-daemon --debug in a NixOS-VM run by qemu-kvm. Because I can define the NixOS-VM essentially just like every NixOS-Host, I can simply enable docker with a one-liner.

    Also check out the reference for NixOS Integration Tests and perhaps helpful blog posts like this one.