Slightly Nerdy

NixOS is great for self-hosting stuff

Posted:

This isn't a how-to or guide, just a show and tell of something I think is cool.

I've been using NixOS for a few years now. I continue to find it frustrating, in large part because I still haven't internalised the language and how everything hangs together to build a system. Maybe I'll get there eventually, but in the mean time I know just enough to be dangerous assemble useful collections of services.

For a server, there's something magical about having pre-written modules to enable and configure a whole bunch of services.

This week, after a particularly trying experience with Google Docs at work, I wanted to try out HedgeDoc. So I threw up a copy on my home server by writing this file and dropping it into my Nix repo, building on what I've setup previously...

{ ... }:
{
  # I run a bunch of services inside a NixOS container,
  # which is "system" container (ala Incus or Proxmox)
  # rather than an application container (ala Docker)
  # I've got a wildcard DNS entry pointing to this
  # container's IP
  containers.intsvcs = {
    bindMounts = {
      "/var/lib/hedgedoc" = {
        # on the host /services is a ZFS dataset, and
        # I create a dataset for each application.
        # By being in here, it'll get snapshotted
        # every 10 minutes and sent over to my Netcup VPS
        # for fast recovery if I need it, in addition
        # to a daily snapshot that's captured by restic,
        # stored locally and rsync'd to a Hetzner Storage
        # Box
        hostPath = "/services/hedgedoc";
        isReadOnly = false;
      };
    };
    config = {
      # I use traefik as my frontend load balancer, it's
      # configured elsewhere with it's basic settings and 
      # wired up with a wildcard LetsEncrypt cert managed
      # by the NixOS `services.acme` module.
      # Here, I just need to tell it about my new service
      services.traefik.dynamicConfigOptions.http = {
        services.docs = {
          loadBalancer = {
            servers = [ { url = "http://localhost:3001"; } ];
          };
        };
        routers.docs = {
          rule = "Host(`docs.mydomain`)";
          service = "docs";
          tls = { };
        };
      };
      services.gatus.settings.endpoints = [
        # I use Gatus to keep an eye on my services, alert
        # me if they're down, or the certifate renewal is
        # getting nearer than it really should. I just need
        # to add an entry into it's endpoints list...
        {
          name = "Hedgedoc";
          group = "Intsvcs";
          interval = "30s";
          url = "https://docs.mydomain";
          conditions = [
            "[STATUS] == 200"
            "[CERTIFICATE_EXPIRATION] > 240h"
          ];
          alerts = [ { type = "ntfy"; } ];
        }
      ];
      services.victoriametrics.prometheusConfig.scrape_configs = [
        # victoria metrics collects all the metrics from my
        # various services, we just need to let it know
        # about this new one...
        {
          job_name = "hedgedoc";
          static_configs = [ { targets = [ "http://127.0.0.1:3001/metrics" ]; } ];
          relabel_configs = [
            {
              source_labels = [ "__address__" ];
              regex = ".*";
              target_label = "instance";
              replacement = "intsvcs";
            }
          ];
        }
      ];
      services.postgresql = {
        # quite a few of the services running in this container
        # make use of postgres, sometimes the NixOS module does all
        # the work for me, other times, like this one, I just need
        # to add to the existing configuration to let it know I'd like
        # a new DB and user (relies on a system user, which the hedgedoc
        # module will create for us)
        ensureUsers = [
          {
            name = "hedgedoc";
            ensureDBOwnership = true;
          }
        ];
        ensureDatabases = [ "hedgedoc" ];
      };
      services.hedgedoc = {
        # and finally the service itself, a handful of lines
        # here to get the hedgedoc config right for my selfup
        # and we're good to go.
        enable = true;
        settings = {
          port = 3001;
          db = {
            dialect = "postgresql";
            host = "/run/postgresql";
            database = "hedgedoc";
          };
          domain = "docs.mydomain";
          protocolUseSSL = true;
          allowOrigin = [ "docs.mydomain" ];
          allowAnonymous = false;
          # not shown are a handful of extra lines configuring
          # it for OAuth2 authentication with Keycloack, the one
          # bit of manual setup I had to do, creating a new client
          # there and dropping in the details here
        };
      };
    };
  };
}

A minute of nixos-rebuild later, and HedgeDoc is up and running.

I like how I can divide up the configuration for common components (Traefik, PostgreSQL, Gatus and VictoriaMetrics) so that the relevant parts live along the services they're being used for.

If I wanted to get fancier, I could save myself some boilerplate and make a module with a small handful of inputs (hostnames, ports, paths) that turns all the common stuff into a couple of lines.

Maybe I'll do that next, or maybe it's time I got around to having native PostgreSQL backups instead of relying on just ZFS snapshots, services.postgresqlBackup should make that a breeze.

And if I decide in a couple of weeks that HedgeDoc isn't for me? I can just delete the file, nixos-rebuild and it's gone.

---