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.