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.

1 Like

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.

1 Like
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.

more examples helping me: https://github.com/NixOS/nixpkgs/blob/8694fb4187be13dec82b75ccef37fc5509f12e43/pkgs/build-support/docker/examples.nix

about buildEnv Nixpkgs 23.11 manual | Nix & NixOS

This thread is the top result on Google for relevant search terms, so for posterity let me propose a solution that works as of 23.11: filesets. Here is an example docker.nix file demonstrating one way to solve @ethanabrooks’ problem:

let
  pkgs = import <nixpkgs> {};
  app = pkgs.lib.fileset.toSource {
    root = ./.;
    fileset = ./app;
  };
in pkgs.dockerTools.buildLayeredImage {
  name = "example";
  tag = "latest";

  contents = [
      app
      pkgs.tcl
      pkgs.toybox
  ];

  config = {
    Env = [
      "PATH=/bin/"
      "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
    ];
    Cmd = [
      "tclsh" "/app/whatever.tcl"
    ];
  };
}

This will place the app directory in the container at /app, similar to using the Dockerfile ADD or COPY instructions.

Note: I used buildLayeredImage and contents here instead of buildImage and copyToRoot because I think this pattern is actually what most people looking to get started with Nix and Docker want, but it doesn’t really matter because the fileset approach has the same result either way.

2 Likes