Flake eachDefaultSystem and single system docker

For a flake I’m writing I want to package my project for both local use on any default system as well as have target for linux/amd64 Docker runtimes. As such, I started out writing a flake making use of flake-utils and its eachDefaultSystem util.
However, my Docker output should not be built for each system, rather, it should only be built for aarch64-linux.
How would I accomplish this? To my mind I should somehow merge the outputs generated from the eachDefaultSystem util with some manual (?) buildLayeredImage output targeting aarch64-linux only.

My question to you all; is this the right way to approach this and how would I go about achieving such?

While I think the question is pretty general, I have included a slightly redacted version of my current flake below. As you can see, the docker image is currently built for each default system, this breaks on macOS because tini is not availble there, but is undesirable in general for my use.

Thanks!

{
  description = "My project";

  inputs = {
    cargo2nix.url = "github:cargo2nix/cargo2nix/release-0.11.0";
    devshell.url = "github:numtide/devshell";
    flake-utils.follows = "cargo2nix/flake-utils";
    nixpkgs.follows = "cargo2nix/nixpkgs";
  };

  outputs = inputs: with inputs;
    flake-utils.lib.eachDefaultSystem (system:
    let
      pkgs = import nixpkgs {
        inherit system;
        overlays = [ 
          devshell.overlays.default 
          cargo2nix.overlays.default
        ];
      };

      rustPkgs = pkgs.rustBuilder.makePackageSet {
        rustVersion = "1.75.0";
        # generated up front with cargo2nix
        packageFun = import ./my-project/Cargo.nix;
        packageOverrides = pkgs: pkgs.rustBuilder.overrides.all ++ [
            # parentheses disambiguate each makeOverride call as a single list element
            (pkgs.rustBuilder.rustLib.makeOverride {
                name = "cmake";
                overrideAttrs = drv: {
                  propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
                    pkgs.cmake
                    pkgs.openssl
                  ];
                };
            })
          ];
      };
    in rec {
      name = "My Project name";

      packages = {
        client = (rustPkgs.workspace.my-project {});
        docker = pkgs.dockerTools.buildLayeredImage {
          name = "registry.company.local/tenant/my-project";
          tag = "0.0.1-snapshot";
          created = builtins.substring 0 8 self.lastModifiedDate;
          config.Entrypoint = "${pkgs.tini}/bin/tini --";
          config.Cmd = "${packages.client}/bin/my-project";
          contents = with pkgs; [ tini ];
        };
        default = packages.client;
      };

      devShells = rec {
        rustShell = pkgs.devshell.mkShell {
          commands = [];
          env = [
          {
            name = "AMQP_ADDR";
            value = "amqp://<user>:<password>@some.broker.io:5672/my-vhost";
          }
          {
            name = "RUST_LOG";
            value = "info";
          }
          ];
          packages = with pkgs; [ 
            rabbitmq-server 
            cargo 
            packages.client
          ];

        };
        default = rustShell;
      };
    }
    );
}

Just merge a second attribute set into it:

outputs = flake-utils.lib.eachDefaultSystem(system: {...}) // {
  packages.aarch64-linux.docker = ...;
}

Thanks for the suggestion @TLATER
It doesn’t quite work in that way, however, as the latter in that update operator overwrites the entire packages attribute in the former attribute set. It doesn’t merge the two packages attributes together.
This results in the following error when simply running nix build:

error: flake 'git+file:///.../my-project' does not provide attribute 'packages.aarch64-darwin.default' or 'defaultPackage.aarch64-darwin'

Instead, I’d expect this to results in building the default = client; output on my aarch64-darwin system (macOS with Apple Silicon).

A simple example in a repl shows the same:

nix-repl> attr1 = { packages.a = rec { client = "foo"; default = client; }; }

nix-repl> attr2 = { packages.b.docker = "bar"; }

nix-repl> (attr1 // attr2).packages
{ b = { ... }; }

I’d like an output like { a = { client = "foo"; default = client; }; b = { docker = "bar"; }; }

Ah, whoops, yeah you’re right of course. I’ve only ever subbed in full outputs so I never noticed, my bad.

Use this library function instead of //: Nixpkgs Manual

That looks great and appears to work, at least on my macOS. nix build works and nix build .#docker errors on the flake not providing the docker attribute, as expected.
I will report back on the docker image building properly when I try it on an aarch64-linux system at some point.

See below for redacted version of earlier flake using the recursiveUpdate. I’ll probably switch over to streamLayeredImages and pipe it directly into something like Skopeo when running it on my CI/CD.

Thanks!

{
  description = "My Flake";

  inputs = {
    cargo2nix.url = "github:cargo2nix/cargo2nix/release-0.11.0";
    devshell.url = "github:numtide/devshell";
    flake-utils.follows = "cargo2nix/flake-utils";
    nixpkgs.follows = "cargo2nix/nixpkgs";
  };

  outputs = inputs: with inputs;
    nixpkgs.lib.attrsets.recursiveUpdate
      (flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ 
            devshell.overlays.default 
            cargo2nix.overlays.default
          ];
        };

        rustPkgs = pkgs.rustBuilder.makePackageSet {
          rustVersion = "1.75.0";
          packageFun = import ./my-project/Cargo.nix;
          packageOverrides = pkgs: pkgs.rustBuilder.overrides.all ++ [
              # parentheses disambiguate each makeOverride call as a single list element
              (pkgs.rustBuilder.rustLib.makeOverride {
                  name = "cmake";
                  overrideAttrs = drv: {
                    propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
                      pkgs.cmake
                      pkgs.openssl
                    ];
                  };
              })
            ];
        };
      in rec {
        name = "My Name";

        packages = {
          client = (rustPkgs.workspace.my-project {});
          default = packages.client;
        };

        devShells = rec {
          rustShell = pkgs.devshell.mkShell {
            commands = [];
            env = [
            {
              name = "AMQP_ADDR";
              value = "amqp://<user>:<password>@amqp.broker.io:5672/vhost";
            }
            {
              name = "RUST_LOG";
              value = "info";
            }
            ];
            packages = with pkgs; [ 
              rabbitmq-server 
              cargo 
              packages.client
            ];

          };
          default = rustShell;
        };
      }
      ))
      { 
        outputs.packages.aarch64-linux.docker = pkgs.dockerTools.buildLayeredImage {
          name = "registry.private.io/tenant/my-project";
          tag = "0.0.1-snapshot";
          created = builtins.substring 0 8 self.lastModifiedDate;
          config.Entrypoint = "${pkgs.client}/bin/tini --";
          config.Cmd = "${packages.client}/bin/my-project";
          contents = with pkgs; [ tini ];
        };
      };
}