Use nixpkgs Nginx module to produce a `nginx.conf`

I want to be able to run nginx in a dev shell for testing. It is simple to do that by including pkgs.nginx in the packages list, and passing it a conf file. However I’d like to be able to share some config between local dev and the production environment (which configures Nginx using the Nixpkgs NixOS module).

Is there a way to talk to that module directly and gain access to the nginx.conf derivation, and provide the same config I use for the production service? I’ve figured out how to reference it with callPackage, but the contents of the attrset that yields seem pretty opaque. Or perhaps this is the wrong approach entirely.

There’s a lot of extra* settings in the nginx module, which you can pass text to. Pick whatever fits your usecase best.

That’s sort of the opposite of what I need. I don’t want to write Nginx config file(s), I want to use the Nginx NixOS module to produce an Nginx file that I can then pass directly as an argument to Nginx.

I’ve made some progress troubleshooting. My flake has a test output currently:

      test = forAllSystems (pkgs:
        {
          test = pkgs.callPackage "${pkgs.path}/nixos/modules/services/web-servers/nginx/" {services.nginx.enable = true;};
        }
      );

When I build it, I get an error message.

$ nix build '.#test.aarch64-linux.test.config.content.environment.etc."nginx/nginx.conf".content.source'

error:
       … while evaluating a branch condition
         at /nix/store/mn45pdsl9nx5j8mwkylxi715nj9vpk5i-qjg5hnnkydk3mri5k6rydhj08x9s7xya-source/nixos/modules/services/web-servers/nginx/default.nix:162:6:
          161|   configFile =
          162|     (if cfg.validateConfigFile then pkgs.writers.writeNginxConfig else pkgs.writeText) "nginx.conf"
             |      ^
          163|       ''

       error: attribute 'services' missing
       at /nix/store/mn45pdsl9nx5j8mwkylxi715nj9vpk5i-qjg5hnnkydk3mri5k6rydhj08x9s7xya-source/nixos/modules/services/web-servers/nginx/default.nix:11:9:
           10| let
           11|   cfg = config.services.nginx;
             |         ^
           12|   inherit (config.security.acme) certs;

I’ve fiddled around with the shape of the attrset I pass in. If I change it to config.services.nginx.enable, it seems to override all of the default configs and it breaks in a different way. I also tried using override on the package instead but that did not change the behavior.

You’d need to evaluate the full NixOS configuration and get a handle on something that exposes the file.

If you enableReload, the file is exposed in environment.etc: nixpkgs/nixos/modules/services/web-servers/nginx/default.nix at 09eb77e94fa25202af8f3e81ddc7353d9970ac1b · NixOS/nixpkgs · GitHub

You could probably get a handle on that via:

(evaluatedModule).config.environment.etc."nginx/nginx.conf".source

If you don’t set enableReload, you could try to pass the nginx config attrset to pkgs.writers.writeNginxConfig, but a lot of the magic happens in let bindings so that probably isn’t very feasible.

So, my suggestion would be to evaluate the module system and set the enableReload in the module set you evaluate (doesn’t have to be the actual config).

In either case, you’re mucking with module internals, and all of this may change with upstream changes.


In the general case, it’s technically possible to create read-only options in NixOS, and those would be an excellent place to put things like this generated file; it’d be neat if more modules did so, but doing so also means exposing it as a public API thing; I think few module authors are motivated to promise you this kind of access.

You could submit a PR that exposes this for nginx, and you could also start an RFC standardizing the handling of configuration files (and final configuration attrsets) such that they’re always available via a read-only option. But that’s not the state today.

1 Like

I’d like to do this outside of the context of a full NixOS configuration. It seems like pkgs.lib.evalModules might be the way to go, but the documentation is thin and I’m not seeing any clear instructions for its input.

Why can’t you create a common module and share the module in both NixOS configs?

@waffle8946 that’s exactly what I want to do. The problem is getting ahold of the config file derivation produced by the module.

My current (non-working) flake code is

      test = forAllSystems (pkgs:
        let
          nginx = (pkgs.callPackage "${pkgs.path}/nixos/modules/services/web-servers/nginx/" {});
          merged = pkgs.lib.evalModules {
            modules = [
              {config.services.nginx.enable = true; config.services.nginx.enableReload = true;}
              nginx
            ];
          };
        in {
          test = merged;
        }
      );

error: 'test.aarch64-linux.test.type' is not a string but a set

Getting maybe closer.

Flake:

      test = forAllSystems (pkgs:
        let
          nginx = import "${pkgs.path}/nixos/modules/services/web-servers/nginx/";
          merged = pkgs.lib.evalModules {
            modules = [
              nginx
              {services.nginx = {
                enable = true;
                enableReload = true;
              };}
            ];
          };
        in {
          test = merged;
        }
      );

Run:

$ nix build '.#test.aarch64-linux.test.config'

error:
       … while calling the 'seq' builtin
         at /nix/store/qjg5hnnkydk3mri5k6rydhj08x9s7xya-source/lib/modules.nix:361:18:
          360|         options = checked options;
          361|         config = checked (removeAttrs config [ "_module" ]);
             |                  ^
          362|         _module = checked (config._module);

       … while calling the 'throw' builtin
         at /nix/store/qjg5hnnkydk3mri5k6rydhj08x9s7xya-source/lib/modules.nix:333:13:
          332|           else
          333|             throw baseMsg
             |             ^
          334|         else

       error: The option `assertions' does not exist. Definition values:
       - In `<unknown-file>':
           [
             {
               assertion = true;
               message = ''
                 The option definition `services.nginx.recommendedZstdSettings' in  no longer has any effect; please remove it.

I think creating a NixOS container config with only Nginx and grabbing just the config might be simpler; you won’t be building the entire NixOS configuration anyway.

You don’t even have to go that far, it’s not like any of this will be built if you just grab the .config attribute. Just evaluating NixOS with only the nginx config set should be enough, no need to create a container config.

It might be a little faster to evaluate if you don’t import all modules, but I also don’t think it’s worth the effort.

Well OK, I do not think in terms of specifically Nginx, I use the trick enough that there are some cases where full /etc/ file name list needs to be evaluated and filtered to request the contents of only one file, so I say that going all the way is not that bad anyway rather than checking that in the specific case you can go a bit less far.

As long as you work within the evaluation context, and what you’re looking for is exposed in an option, you don’t need a specific configuration. That solution is fairly generic, not specifically for nginx.

This should be true for anything exposed in NixOS, somewhere down the line, though some things might end up as raw strings in the activationScript or a line in a systemd.service.script because they’re bound in a let and therefore are not easily digestible; for those you have no way around patching the module to expose this, or actually running a NixOS system, and yeah, there a container might make sense. That’s far from true for configuration files simply exposed via environment.etc though.

This is possible but unreasonable, because it depends on fragile impl details - and nixos currently does not provide a way to independently use modules like this.

I’d just write the full config, like

nixpkgs.lib.nixosSystem {
  modules = [
    (
      { modulesPath, ... }:
      {
        # minimal base config
        imports = [ "${modulesPath}/profiles/minimal.nix" ];

        boot.loader.grub.enable = false;
        fileSystems."/".device = "a";
        nixpkgs.hostPlatform = "x86_64-linux";
        system.stateVersion = "25.11";

        # custom config here
        services.nginx = {
          enable = true;
          enableReload = true;
        };
      }
    )
  ];
}

Then you can wrap that in parens and access attrs from config.

If you really want to use evalModules directly and only import the applicable modules, you’ll need to figure out what those several-dozen modules are yourself. And you’ll also need to set some important module args as well, most-likely.

5 Likes

Thanks so much for the discussion on this everyone, and especially to @waffle8946 for putting together a clear example. I have a working solution, here’s my exact code.

# lib/nginx_conf.nix
{
  nixpkgs,
  platform,
}:
nginxConfig:
(nixpkgs.lib.nixosSystem {
  modules = [
    {
      nixpkgs.hostPlatform = platform;
      system.stateVersion = "25.11";
      services.nginx = nginxConfig // {
        enable = true;
        enableReload = true;
      };
    }
  ];
}).config.environment.etc."nginx/nginx.conf".source
# flake.nix
      test = forAllSystems (pkgs: {
        test = (
          import ./lib/nginx_config.nix {
            nixpkgs = unstable;
            platform = pkgs.stdenv.hostPlatform.system;
          } { virtualHosts.localhost.locations."/".tryFiles = "index.html =404"; }
        );
      });

Running the build command produces a normal looking nginx config file.

$ nix build '.#test.aarch64-linux.test'; cat result
pid /run/nginx/nginx.pid;
error_log stderr;
daemon off;
events {
}
http {
        # Load mime types and configure maximum size of the types hash tables.
        include /nix/store/99gwh9gsl65zj5fd4jwdf1lnkz5yix7g-mailcap-2.1.54/etc/nginx/mime.types;
        types_hash_max_size 2688;
        include /nix/store/chi69rgang6ndl768mkia21lixvbidaq-nginx-1.28.0/conf/fastcgi.conf;
        include /nix/store/chi69rgang6ndl768mkia21lixvbidaq-nginx-1.28.0/conf/uwsgi_params;
        default_type application/octet-stream;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
        # $connection_upgrade is used for websocket proxying
        map $http_upgrade $connection_upgrade {
                default upgrade;
                ''      close;
        }
        client_max_body_size 10m;
        server_tokens off;
        server {
                listen 0.0.0.0:80 ;
                listen [::0]:80 ;
                server_name localhost ;
                location / {
                        try_files index.html =404;
                }
        }
}
1 Like