Simplifying Caddy configs in NixOS

I refactor my Caddy configs from the unnecessarily complex method to something a whole lot more simpler


A gentle reminder

I’ve written before about how I use Caddy to reverse proxy my internal services. Since then I’ve migrated to NixOS and am using that to automatically generate the Caddy config.

However the approach I used was extremely over-engineered and unnecessarily complicates one of the benefits of Caddy, which is the simplicity of its Caddyfile DSL. It was interesting to see how I could generate configs from Nix attribute sets, which served as a learning exercise and the basis for the Nix cheat sheet.

NixOS allows you to specify Caddy config in a way that allows you to modularise it. Migrating to this structure will provide cleaner code for future iterations.

Previously I had created the module for Caddy in such a way that it would discover what services required a reverse proxy fronting it on said host. This snippet from the previous config would look up from the catalog for any services that were due to be hosted on this machine, then build the Caddy config from there.

host_services = attrValues (filterAttrs (svc_name: svc_def:
  svc_def ? "host" && == config.networking.hostName
  && svc_def.caddify.enable);

I have seen the light

Instead of doing all the building of the Caddy JSON config by hand, I can declare a block in each service module that requires it which will ensure Caddy will create a reverse proxy for it. This means a module now encapsulates everything that is required for this service to function properly.

Such a module would then look like the below, with some lines removed for brevity. You can see it in full in GitHub.

{ config, pkgs, lib, ... }:

with lib;

let cfg = config.modules.plex;
in {

  options.modules.plex = { enable = mkEnableOption "Deploy plex"; };

  config = mkIf cfg.enable {

    services.caddy.virtualHosts."".extraConfig = ''
      tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
      reverse_proxy localhost:32400

    services.plex = {
      enable = true;
      openFirewall = true;

This is a lot simpler, and it meant that I was able to remove a lot of complicated Nix code in the pull request to achieve the same thing. The end result is a solution that is much easier to understand and pragmatic.

I’m still using a similar method for when writing the AdGuardHome DNS config, but I changed the functions to more appropriate names. You can revisit the previous blog post where I walk through that.

This refactoring to more complete modules isn’t ground-breaking by any means, but I wanted to share this win I gained 👍.