Adding Custom Nginx Options in NixOS

NixOS comes with a pretty good set of options for configuring nginx the nix way, but there are still plenty of things for which you have to fall back to using the extraConfig option and write plain nginx config lines.

This is fine most of the time, but if you have a larger set of options that you're applying often, it can be a lot nicer to have proper nix options for it.

Luckily the NixOS option is flexible enough to allow you to define your own set of options to be added to the nginx. You can define your own services.nginx.virtualHosts and services.nginx.virtualHosts.<name>.locations submodules which nix will recursively merge into the upstream options. And to add the config lines from your custom option to the generated nginx configuration, you can set the extraConfig value in the config field of the submodule.

For example, here is a NixOS module that adds a boolean limitToLan option both to the virtualHosts and locations submodules, which when enabled configures nginx to only allow clients from specific ip ranges to access the site.

{
  config,
  lib,
  ...
}:
with lib; let
  hostOptions = {name, ...}: let
    hostCfg = config.services.nginx.virtualHosts.${name};
  in {
    options =
      {
        locations = mkOption {
          type = types.attrsOf (types.submodule (locationOptions name));
        };
      }
      // (addedOptions hostCfg);
    config = addedConfig hostCfg;
  };
  locationOptions = host: {name, ...}: let
    locationCfg = config.services.nginx.virtualHosts.${host}.locations.${name};
  in {
    options = addedOptions;
    config = addedConfig locationCfg;
  };
  addedOptions = cfg: {
    limitToLan = mkOption {
      type = types.bool;
      default = false;
      description = "Only allow lan/vpn clients";
    };
    extraConfig = mkOption {
      apply = value: let
        cfgLines = optionalString cfg.limitToLan ''
          allow 10.0.8.0/24; # vpn
          allow 127.0.0.0/8; # localhost
          allow 172.16.0.0/12; # docker
          allow 192.168.1.0/24; # lan
          deny all;
        '';
      in
        value + cfgLines;
    };
  };
  addedConfig = cfg: {
    extraConfig = optionalString cfg.limitToLan ''
      allow 10.0.8.0/24; # vpn
      allow 127.0.0.0/8; # localhost
      allow 172.16.0.0/12; # docker
      allow 192.168.1.0/24; # lan
      deny all;
    '';
  };
in {
  options = {
    services.nginx.virtualHosts = mkOption {
      # nix will merge the `type` we set with the `type` of the upstream options.
      type = types.attrsOf (types.submodule hostOptions);
    };
  };
}

Having a separate config option reduces the clutter in the actual configuration and makes it easy to change the allow ip ranges for all sites that you want to have protected without having to update each site individually.