How do you use pkgs.dockerTools to express a multi-stage build process similar to a Dockerfile?

I have been trying to find an example of pkgs.dockerTools.pullImage, pkgs.dockerTools.buildImage, and pkgs.dockerTools.buildLayeredImage that does the equivalent of this very simple dockerfile:

FROM caddy:2.11-builder AS builder
RUN xcaddy build \
    --with github.com/caddy-dns/namesilo \
    --with github.com/liujed/caddy-dns01proxy

FROM caddy:2.11
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

So far, I’ve found the following threads and posts on the topic:

My goal is to work with this config for home-manager (that link is to the commit in my repo that is from, so if more context is needed you can view the rest of the source there):

{ config, lib, osConfig, ... }:

let
  # The "users" group.
  gid = "100";
  uid = toString osConfig.system-settings.users.${config.home.username}.uid;
in
{
  config.services.podman.containers.caddy-reverese-proxy = lib.mkIf (
    osConfig.system-settings.server-environment.enable &&
    osConfig.system-settings.server-environment.containers.enable &&
    config.home.service-user.containers.caddy.enable
  ) {
    image = "docker.io/library/caddy:2.11";
    addCapabilities = [
      "NET_ADMIN"
    ];

    autoStart = true;
    autoUpdate = "registry";
    description = "Caddy Reverse Proxy";
    extraPodmanArgs = [
      "--name=caddy-reverse-proxy"
      "--user=${uid}:${gid}"
      "--userns=keep-id"
    ];

    network = "hosted-applications";
    networkAlias = [
      "caddy"
      "caddy-reverse-proxy"
    ];

    ports = [
      "80:80"
      "443:443"
      "443:443/udp"
    ];

    volumes = [
      "/srv/configuration/caddy/config:/config:Z"
      "/srv/configuration/caddy/data:/data:Z"
      "/srv/configuration/caddy/etc:/etc/caddy:Z"
      "/srv/configuration/caddy/logs:/logs:Z"
      "/srv/www:/srv:z"
    ];
  };
}

The end goal is to replace “image” in the above config with the equivalent of the Dockerfile I posted earlier. Any help would be greatly appreciated.

Docker containers are zipfiles with a well-defined layout.

This layout demands an index file, and a bunch of directories referred to from this index which are copied on top of each other to create a root filesystem.

A “multi-stage build” in the docker world runs one of these filesystems in something similar to a VM, runs some code in it to produce an output, and then copies files from this VM to another rootfs.

This does not have a clean equivalent in the nix world, because nix does not do things imperatively; there are no “stages” to go through. dockertools builders just build the tarball from existing contents.

What you want to achieve is not a “multi-stage build”, you want to produce an image with equivalent contents. In this case, that means building a custom fork of caddy. To do that, you’re running a docker container, which produces a file, and then you copy that file into the zipfile.

In the nix world, that’d in theory look something like this:

{ lib, pkgs, ... }:
let
  # *very* pseudocode
  caddy = pkgs.runCommand "caddy-fork" ''
    ${lib.getExe pkgs.docker} run caddy:2.11-builder --lotsofargsthatrunthisbuildandproduceanoutput
    cp mount/usr/bin/caddy $out/bin/caddy
  '';
in
  pkgs.dockertools.buildLayeredImage {
    name = "caddy";
    contents = [
      caddy
    ];
  }

The problem is that, to achieve this, you have to run docker as part of a nix build. This might be possible with podman, but I would really not recommend attempting it.

Why don’t you use the xcaddy packaged in nixpkgs instead, and use that to produce the binary you put in your container?

Perfect, I think that answers my question. A quick try at it and something very similar to this worked for my use case. I appreciate it!

EDIT: For clarity, I mean that using the nixpkgs xcaddy worked for my purposes.