COPY files into a Docker container using `copyToRoot`

Based on dockerTools.buildImage ADD/COPY equivalent, it looks like the good ole fashioned way to add local files to a Nix-produced docker container was contents. However, according to this warning, the contents parameter is deprecated:

warning: in docker image: The contents parameter is deprecated. Change to copyToRoot if the contents are designed to be copied to the root filesystem, such as when you use `buildEnv` or similar between contents and your packages. Use copyToRoot = buildEnv { ... }; or similar if you intend to add packages to /bin.

When I try to do what I thought would be the equivalent:

{
  pkgs ? import <nixpkgs> {},
  pkgsLinux ? import <nixpkgs> {system = "x86_64-linux";},
}:
pkgs.dockerTools.buildImage {
  name = "hello-docker";
  tag = "latest";
  copyToRoot = pkgs.buildEnv {
    name = "image-root";
    pathsToLink = ["/"];
    paths = [pkgs.coreutils ./hello.sh];
  };
  config = {
    Env = ["PATH=/bin/"];
    Cmd = [
      "${pkgs.bash}/bin/bash"
    ];
  };
}

I get:

error: The store path /nix/store/pd8ny76d3skhk5r7xqg625p3fhla1igp-hello.sh is a file and can't be merged into an environment using pkgs.buildEnv! at /nix/store/2s4zc82jpfvdz3pavxc2vqvm6fq45syv-builder.pl line 122.

I have looked at the buildImage documentation and what I think is the buildEnv source code but haven’t been able to figure out what I’m doing wrong. Thanks for your help!

paths is supposed to be list of directories to merge together. ./hello.sh is a file, not a directory.

I see. This works then:

{
  pkgs ? import <nixpkgs> {},
  pkgsLinux ? import <nixpkgs> {system = "x86_64-linux";},
}:
pkgs.dockerTools.buildImage {
  name = "hello-docker";
  tag = "latest";
  copyToRoot = pkgs.buildEnv {
    name = "image-root";
    pathsToLink = ["/hello" "/"];
    paths = [pkgs.coreutils ./.]; # ./. not ./hello.sh
  };
  config = {
    Env = ["PATH=/bin/"];
    Cmd = [
      "${pkgs.bash}/bin/bash"
    ];
  };
}

Although it seems to map everything to /. How would I map ./. to e.g. /app?

pathsToLink is the list of subdirectories to be merged. So when you put "/" in there, you’re saying everything should get merged, making "/hello" redundant. So paths is the list of paths you want to get files from, and pathsToLink is the list of subdirectories within those paths that will be merged and linked. e.g.

pkgs.buildEnv {
  name = "foobar";
  paths = [pkgs.coreutils pkgs.util-linux];
  pathsToLink = ["/bin"];
}

Produces a path with just a bin directory, which contains symlinks to all the files in the bin directories of coreutils and util-linux.

Right that makes sense. I actually meant to remove /hello. But is there a way to map ~/my-project/hello.sh to /app/hello.sh inside the container?

You could just put hello.sh into ~/my-project/app/hello.sh, using something like paths = [./. pkgs.coreutils]; and pathsToLink = ["/app" "/bin"];. But I don’t like using ./. because that puts your whole project directory into the nix store every time you do a build. You can fix that either by using builtins.filterSource on ./. to make sure only the app subdir is included, or you can just make a derivation that puts the file in the right place like this:

pkgs.writeTextDir "app/hello.sh" (builtins.readFile ./hello.sh)

Asign that to a variable and put that variable in paths instead of ./..

Ok. In my application, I actually have all my source code in a subdirectory so I should be able to just link that subdirectory with pathsToLink. Thanks!

This is the solution that worked for me:

{
  pkgs ? import <nixpkgs> {},
  pkgsLinux ? import <nixpkgs> {system = "x86_64-linux";},
}:
   pkgs.dockerTools.buildImage {
    name = "hello-docker";
    tag = "latest";
    copyToRoot = pkgs.buildEnv {
      name = "image-root";
      pathsToLink = ["/app" "/bin"];
      paths = [pkgs.coreutils ./.];
    };
    config = {
      Env = ["PATH=/bin/"];
      Cmd = [
        "${pkgs.bash}/bin/bash"
      ];
    };
  }

Here is my directory structure:

.
├── app
│   └── hello.sh
├── big-file-to-ignore
└── hello-docker.nix

I tried adding

let
  app = builtins.filterSource (path: type: (baseNameOf path) == "app") ./.;
in
...
    copyToRoot = pkgs.buildEnv {
      name = "image-root";
      pathsToLink = ["/" "/bin"];
      paths = [pkgs.coreutils app];
    };
...

but /app was empty inside the container (no hello.sh). Maybe I am not using filterSource correctly.

builtins.filterSource (path: type: (baseNameOf path) == "app") ./.

This will include the directory, but exclude its children, because the baseNameOf "/app/hello.sh" is "hello.sh", not "app".

So how would I include /app/hello.sh without also including big-file-to-ignore (and without copying files one-by-one with something like pkgs.writeTextDir)? I added .dockerignore but nix seems to ignore it.

Hosted by Flying Circus.