How to create a module of the type `services.module.<name>`?

Background

I want to create separate nixos-containers for each of the service I’ll host on my server.
Writing nixos-container configurations is very repetitive though, so I thought about creating a module that will make it easier to set them up.
I realised that even if I create a services.custom-container module, I can only define a single container. (Implemented here: https://codeberg.org/BlastboomStrice/dotfiles/src/commit/432096d86b25cefdb2036ded50944645de5214dc/.config/nixos-config/modules/nixos/containers/container-stirling-pdf.nix)
I want to create a module that will let me define multiple containers from a single module and set up something in my configuration.nix file like this:

services.custom-containers = {
  containerA = {
    # options
  };
containerB = {
  # options
};
};

I would like to avoid creating a seperate module for each container:

Counter-example
services.custom-containerA = {
  # options
};
services.custom-containerB = {
  # options
};

My whole configuration is hosted here: https://codeberg.org/BlastboomStrice/dotfiles
(Btw, I will probably (or may not) make another post later on how to inject freeform code inside a module, to inject configuration of various services inside different nixos-containers I will create. I’m not very sure if that’s the path I want to follow, but that whole thing is not that important in this post.)

Issue

My main issue is that I don’t know how to generate multiple containers from a single module.
The official nixos-containers options work exactly like that (containers.<name>).

  1. How do I set up a module like that? A module that will generate multiple nixos-containers (with some preset values or values from some custom options of mine).
  2. How can each container be assigned by the name I give when I define it? For example, I’d want the following container generated to be named containerA, where can I find and use the <name> value?
    services.custom-containers = {
      containerA = {
        # options
      };
    };
    

What the docs say

From what I understand, I must create an option (named what though?) of type attrsOf and a submodule nested inside of it with the options I want to pass to the container.
I’m not really sure how to do this.

I’ve checked the manual and the wiki for help:

Things I’ve tried

Since the docs didn’t help me that much to understand what I exactly have to do, I tried to gather inspiration (but failed) from the nixos-container module itself: https://github.com/NixOS/nixpkgs/blob/e73de5be04e0eff4190a1432b946d469c794e7b4/nixos/modules/virtualisation/nixos-containers.nix

I found a very related part in the code:

containers = mkOption {
  type = types.attrsOf (
    types.submodule (

But I didn’t understand how to properly use it (as a sidenote, in the manual it seems to do types.submodule {, not types.submodule (, it uses the curly bracket instead of parenthesis, but whatever).

Other solutions

I could give up and avoid complexity by defining a module per container, but this will have possibly thousands of repeated lines and this makes it hard to manage the code, especially if I want to update something (I’ll have to edit all the modules).

I would start by defining a function that turns this into the correct configurations. From that I think you can still extend to your own module.

Did you look into modules that already achieve this?
E.g. nixpkgs/nixos/modules/services/web-servers/nginx/default.nix at 714a5f8c4ead6b31148d829288440ed033ccc041 · NixOS/nixpkgs · GitHub

You just map over all your attributes. (Here using mapAttrsToList)

1 Like

Thanks for the guidance:)
I know almost nothing about mapattrstolist, I’ll have to check more. (Kinda scares me)

I will probably come back in a day (or two) afrer experimenting with it, cuz I might be busy for a little while:)

If a simple map in functional programming doesn’t scare you then this should certainly not scare you. I don’t know with which programming language you are familiar with but maybe you can try to think of what this would look like in that language.

It is really just mapping over key-value-pairs each corresponding to one attribute in the attribute set. The example that I gave you is a bit extreme, sorry for that, just ignore the huge function applied to each attribute, but what you want to do is probably on the same scale.

Different resources that I found: (they essentially say the same)

1 Like

Correct!

Good! You’re on the right track.

This is all good so far! This is the right thing to use as a model for your solution.

This is the first red flag. Both the manual and the linked code are correct, and this confusion points to not yet being fluent in Nix.

In Nix, parentheses are only written to ensure that expressions are grouped in the intended way. They don’t otherwise make any difference or indicate that any different language element is being used. (There is one exception to this rule: the parentheses in inherit (foo) bar baz;. That’s not important here; I include it only to be totally honest.)

To think correctly about types.submodule, you should understand what types of value it accepts as an argument, not what the first character of that argument should be. If types.submodule arg is correct, types.submodule (arg) is also correct.

The reason there is a parenthesis in the containers module is that types.submodule is being given a function expression as its argument. A function expression needs to be wrapped in parentheses when being given as an argument to a function; the technical description is that ‘function application has higher precedence than function expressions’. Compare to how, because ‘multiplication has higher precedence than addition’, you must include the parentheses in 2 * (3 + 4) if you want the result 14 instead of 10. But you can also write, without parentheses:

let
  a = 3 + 4;
in
2 * a

This shows that there’s nothing about either multiplication or addition, by themselves, that requires parentheses; nor do number values require them. The parentheses are only needed when combining expressions of different precedence, and when the higher-precedence expression should entirely contain the lower-precedence one.

Similarly, the code in the containers module uses parentheses, but just like in the let a = ... example above, it could also have used a let-binding to name the function expression and then used that variable name as the argument to types.submodule, and no parentheses would be required.

Back to the manual, it says that the argument to types.submodule should be

And that’s true! The example given in the manual is of the first variety — an attribute set, represented directly as an attribute set expression. And since an attribute set expression has, effectively, the highest precedence (along with list expressions, parenthesized expressions, and atomic literals like strings and numbers), it can be used as a function argument without parentheses.

That was a long tangent into something that may not seem directly relevant to what you’re trying to do, but I hope it makes you a better Nix programmer to read and understand it, which will in the long run improve all of your future endeavors.

You could keep studying the containers module to understand how it does what it does, but if that’s too much to bite off at once, I would look at examples 29 and 30 in the manual. Continuing those examples, using the values in mod to define containers could look like this:

{
  containers = builtins.mapAttrs (name: modValues: {
    localAddress = "10.0.1.${toString modValues.foo}";
  }) config.mod;
}

This would be the equivalent of:

{
  containers.one.localAddress = "10.0.1.1";
  containers.two.localAddress = "10.0.1.2";
}
3 Likes

Thank y’all, I’ll have to play around with the info you gave me. The truth is I dont know much about nix (I just did the nix dev tutorial) and I barely know about the maps thing.
Hopefully I’ll make it work with enough trying, I’ll try to keep you updated in case something comes up:)

Hm, coming back to this, I think this is too complex for me..
I don’t know nix well enough to do such (advanced for me) stuff, I’ll have to learn nix better before I do.
I found this site https://nixcloud.io/tour/ that may help me to do some extra exercises on nix.

I’d like to finish the server by (around) the end of July, so I guess I can’t spend 1-2 weeks learning more nix, for now. I should update this post later I guess, I might just go with the simple+repetitive solution for now.

1 Like