Skip to content

Instantly share code, notes, and snippets.

@duesee
Last active July 30, 2025 08:21
Show Gist options
  • Save duesee/e5cd05839e9b24f6168e4d0e657a5fe2 to your computer and use it in GitHub Desktop.
Save duesee/e5cd05839e9b24f6168e4d0e657a5fe2 to your computer and use it in GitHub Desktop.

/etc/nix/flake.nix

{
  inputs = {
    # ...
    headplane = {
      # Note: Fork with a small patch, see ...
      # <https://github.com/tale/headplane/compare/main...duesee:headplane:main#diff-344adf682bbe2185cd290e6c26804fac7a8153d490ea6895c521806094b5fbd4R26-R27>.
      url = "github:duesee/headplane/main";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    # ...
  };
  outputs =
    {
      # ...
      headplane,
      # ...
      ...
    }:
    {
      nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
        modules = [
          # ...
          headplane.nixosModules.headplane
          {
            nixpkgs.overlays = [ headplane.overlays.default ];
          }
          # ...
        ];
      };
    };
}

/etc/nix/configuration.nix

{
  ...
}:
{
  imports = [
    # ...
    ./headscale.nix
    # ...
  ];
  
  # ...
}

/etc/nix/headscale.nix

See https://carlosvaz.com/posts/setting-up-headscale-on-nixos/.

Note: Not finished because can't access Let's Encrypt for now.

TODO:

  • ACME.
  • Make accessible via nginx only.
  • Allow configuration via headplane.
{ config, pkgs, ... }:
let
  headscale_port = 10080;
  headplane_port = 20080;
in
{
  # Headscale
  services = {
    headscale = {
      enable = true;
      address = "0.0.0.0";
      port = headscale_port;
      settings.dns = {
        base_domain = "localhost";
        server_url = "http://headscale.localhost";
        nameservers.global = [ "1.1.1.1" ];
        logtail.enabled = false;
      };
    };

    nginx.virtualHosts = {
      "headscale.localhost" = {
        # forceSSL = true;
        # enableACME = true;
        locations = {
          "/" = {
            proxyPass = "http://localhost:${toString headscale_port}";
            proxyWebsockets = true;
          };
        };
      };
    };
  };

  # Headplane
  services = {
    headplane = {
      enable = true;
      agent.enable = false;

      settings = {
        server = {
          host = "0.0.0.0";
          port = headplane_port;
          cookie_secret = "????????????????????????????????";
          cookie_secure = false;
        };
        headscale = {
          url = "http://headscale.localhost:${toString headscale_port}";
          config_path = "/etc/headscale/config.yaml";
          config_strict = "true";
        };
        integration = {
          proc.enabled = true;
        };
        # TODO:
        # oidc = { ... };
      };
    };

    nginx.virtualHosts = {
      "headplane.localhost" = {
        # forceSSL = true;
        # enableACME = true;
        locations = {
          "/" = {
            proxyPass = "http://localhost:${toString headplane_port}";
          };
        };
      };
    };
  };

  networking.firewall.allowedTCPPorts = [
    headscale_port
    headplane_port
  ];

  # Tailscale (for testing only)
  services.tailscale.enable = true;
  networking.firewall = {
    checkReversePath = "loose";
    trustedInterfaces = [ "tailscale0" ];
    allowedUDPPorts = [ config.services.tailscale.port ];
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment