Customize Caddy’s plugins with Nix

Vincent Bernat

Caddy is an open-source web server writ­ten in Go. It han­dles TLS cer­tifi­cates au­to­mat­i­cally and comes with a sim­ple con­fig­u­ra­tion syn­tax. Users can ex­tend its func­tion­al­ity through plug­ins1 to add fea­tures like rate lim­it­ing, caching, and Docker in­te­gration.

While Caddy is avail­able in Nix­p­kgs, adding ex­tra plug­ins is was not sim­ple.2 The com­pi­la­tion process needs In­ter­net ac­cess, which Nix de­nies dur­ing build to en­sure re­pro­ducibil­ity. When try­ing to build the fol­low­ing deriva­tion us­ing xcaddy, a tool for build­ing Caddy with plug­ins, it fails with this er­ror: dial tcp: lookup proxy.​golang.​org on [::​1]:​53: connection refused.

{ pkgs }:
pkgs.stdenv.mkDerivation {
  name = "caddy-with-xcaddy";
  nativeBuildInputs = with pkgs; [ go xcaddy cacert ];
  unpackPhase = "true";
  buildPhase =
    ''
      xcaddy build --with github.com/caddy-dns/powerdns@v1.0.1
    '';
  installPhase = ''
    mkdir -p $out/bin
    cp caddy $out/bin
  '';
}

Fixed-out­put deriva­tions are an ex­cep­tion to this rule and get net­work ac­cess dur­ing build. They need to spec­ify their out­put hash. For ex­am­ple, the fetchurl func­tion pro­duces a fixed-out­put derivation:

{ stdenv, fetchurl }:
stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.12.1";
  src = fetchurl {
    url = "mirror://gnu/hello/hello-${version}.tar.gz";
    hash = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=";
  };
}

To cre­ate a fixed-out­put deriva­tion, you need to set the outputHash at­tribute. The ex­am­ple be­low shows how to out­put Caddy’s source code, with some plugin en­abled, as a fixed-out­put deriva­tion us­ing xcaddy and go mod vendor.

pkgs.stdenvNoCC.mkDerivation rec {
  pname = "caddy-src-with-xcaddy";
  version = "2.8.4";
  nativeBuildInputs = with pkgs; [ go xcaddy cacert ];
  unpackPhase = "true";
  buildPhase =
    ''
      export GOCACHE=$TMPDIR/go-cache
      export GOPATH="$TMPDIR/go"
      XCADDY_SKIP_BUILD=1 TMPDIR="$PWD" \
        xcaddy build v${version} --with github.com/caddy-dns/powerdns@v1.0.1
      (cd buildenv* && go mod vendor)
    '';
  installPhase = ''
    mv buildenv* $out
  '';

  outputHash = "sha256-F/jqR4iEsklJFycTjSaW8B/V3iTGqqGOzwYBUXxRKrc=";
  outputHashAlgo = "sha256";
  outputHashMode = "recursive";
}

With a fixed-out­put deriva­tion, it is up to us to en­sure the out­put is al­ways the same:

  • we ask xcaddy to not com­pile the pro­gram and keep the source code,3
  • we pin the ver­sion of Caddy we want to build, and
  • we pin the ver­sion of each re­quested plugin.

You can use this deriva­tion to over­ride the src at­tribute in pkgs.caddy:

pkgs.caddy.overrideAttrs (prev: {
  src = pkgs.stdenvNoCC.mkDerivation { /* ... */ };
  vendorHash = null;
  subPackages = [ "." ];
});

Check out the com­plete ex­am­ple in the Git repos­i­tory. To in­te­grate into a Flake, add github:​vincentbernat/​caddy-nix as an overlay:

{
  inputs = {
    nixpkgs.url = "nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
    caddy.url = "github:vincentbernat/caddy-nix";
  };
  outputs = { self, nixpkgs, flake-utils, caddy }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ caddy.overlays.default ];
        };
      in
      {
        packages = {
          default = pkgs.caddy.withPlugins {
            plugins = [ "github.com/caddy-dns/powerdns@v1.0.1" ];
            hash = "sha256-F/jqR4iEsklJFycTjSaW8B/V3iTGqqGOzwYBUXxRKrc=";
          };
        };
      });
}

Up­date (2024-11)

This flake won’t work with Nix­p­kgs 24.05 or older be­cause it re­lies on this com­mit to prop­erly over­ride the vendorHash at­tribute.

Up­date (2025-05)

My pull re­quest for Nix­p­kgs was merged and since Nix­p­kgs 25.05, you don’t need to spec­ify the over­lay any­more. Check the com­plete example.