managing home server with nix
This commit is contained in:
parent
f8f9fc8b1e
commit
0055a0f4fc
@ -10,5 +10,5 @@
|
|||||||
listenAddr = ":4000";
|
listenAddr = ":4000";
|
||||||
|
|
||||||
# If empty then a derived static directory is used
|
# If empty then a derived static directory is used
|
||||||
staticProxyURL = "http://127.0.0.1:4001";
|
staticProxyURL = "http://127.0.0.1:4002";
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
-s ./src \
|
-s ./src \
|
||||||
-d ./_site \
|
-d ./_site \
|
||||||
-w -I -D \
|
-w -I -D \
|
||||||
-P 4001
|
-P 4002
|
||||||
'';
|
'';
|
||||||
|
|
||||||
allInputs = depInputs ++ [ jekyllEnv serve ];
|
allInputs = depInputs ++ [ jekyllEnv serve ];
|
||||||
|
266
static/src/_posts/2021-11-08-managing-a-home-server-with-nix.md
Normal file
266
static/src/_posts/2021-11-08-managing-a-home-server-with-nix.md
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
---
|
||||||
|
title: >-
|
||||||
|
Managing a Home Server With Nix
|
||||||
|
description: >-
|
||||||
|
Docker is for boomers.
|
||||||
|
tags: tech
|
||||||
|
---
|
||||||
|
|
||||||
|
My home server has a lot running on it. Some of it I've written about in this
|
||||||
|
blog previously, some of it I haven't. It's hosting this blog itself, even!
|
||||||
|
|
||||||
|
With all of these services comes management overhead, both in terms of managing
|
||||||
|
packages and configuration. I'm pretty strict about tracking packages and
|
||||||
|
configuration in version control, and backing up all state I care about in B2,
|
||||||
|
such that if, _at any moment_, the server is abducted by aliens, I won't have
|
||||||
|
lost much.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Previously I accomplished this with docker. Each service ran in a container
|
||||||
|
under the docker daemon, with configuration files and state directories shared
|
||||||
|
in via volume shares. Configuration files could then be stored in a git repo,
|
||||||
|
and my `docker run` commands were documented in `Makefile`s, because that was
|
||||||
|
easy.
|
||||||
|
|
||||||
|
This approach had drawbacks, notably:
|
||||||
|
|
||||||
|
* Docker networking is a pain. To be fair I should have just used
|
||||||
|
`--network=host` and dodged the issue, but I didn't.
|
||||||
|
|
||||||
|
* Docker images aren't actually deterministically built, so if I were to ever
|
||||||
|
have to rebuild any of the images I was using it I couldn't be sure I'd end up
|
||||||
|
with the same code as before. For some services this is actually a nagging
|
||||||
|
security concern in the back of my head.
|
||||||
|
|
||||||
|
* File permissions with docker volumes are fucked.
|
||||||
|
|
||||||
|
* Who knows how long the current version of docker will support the old ass
|
||||||
|
images and configuration system I'm using now. Probably not the next 10 years.
|
||||||
|
And what if dockerhub goes away, or changes its pricing model?
|
||||||
|
|
||||||
|
* As previously noted, docker is for boomers.
|
||||||
|
|
||||||
|
## Nix
|
||||||
|
|
||||||
|
Nix is the new hotness, and it solves all of the above problems quite nicely.
|
||||||
|
I'm not going to get into too much detail about how nix works here (honestly I'm
|
||||||
|
not very good at explaining it), but suffice to say I'm switching everything
|
||||||
|
over, and this post is about how that actually looks in a practical sense.
|
||||||
|
|
||||||
|
For the most part I eschew things like [flakes][flakes],
|
||||||
|
[home-manager][home-manager], and any other frameworks built on nix. While the
|
||||||
|
framework of the day may come and go, the base nix language should remain
|
||||||
|
constant.
|
||||||
|
|
||||||
|
As before with docker, I have a single git repo being stored privately in a way
|
||||||
|
I'm confident is secure (which is necessary because it contains some secrets).
|
||||||
|
|
||||||
|
At the root of the repo there exists a `pkgs.nix` file, which looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
src ? builtins.fetchTarball {
|
||||||
|
name = "nixpkgs-d50923ab2d308a1ddb21594ba6ae064cab65d8ae";
|
||||||
|
url = "https://github.com/NixOS/nixpkgs/archive/d50923ab2d308a1ddb21594ba6ae064cab65d8ae.tar.gz";
|
||||||
|
sha256 = "1k7xpymhzb4hilv6a1jp2lsxgc4yiqclh944m8sxyhriv9p2yhpv";
|
||||||
|
},
|
||||||
|
}: (import src) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
This file exists to provide a pinned version of `nixpkgs` which will get used
|
||||||
|
for all services. As long as I don't change this file the tools available to me
|
||||||
|
for building my services will remain constant forever, no matter what else
|
||||||
|
happens in the nix ecosystem.
|
||||||
|
|
||||||
|
Each directory in the repo corresponds to a service I run. I'll focus on a
|
||||||
|
particular service, [navidrome][navidrome], for now:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
:: ls -1 navidrome
|
||||||
|
Makefile
|
||||||
|
default.nix
|
||||||
|
navidrome.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Not much to it!
|
||||||
|
|
||||||
|
### default.nix
|
||||||
|
|
||||||
|
The first file to look at is the `default.nix`, as that contains
|
||||||
|
all the logic. The overall file looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
let
|
||||||
|
|
||||||
|
pkgs = (import ../pkgs.nix) {};
|
||||||
|
|
||||||
|
in rec {
|
||||||
|
|
||||||
|
entrypoint = ...;
|
||||||
|
|
||||||
|
service = ...;
|
||||||
|
|
||||||
|
install = ...;
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The file describes an attribute set with three attributes, `entrypoint`,
|
||||||
|
`service`, and `install`. These form the basic pattern I use for all my
|
||||||
|
services; pretty much every service I manage has a `default.nix` which has
|
||||||
|
attributes corresponding to these.
|
||||||
|
|
||||||
|
#### Entrypoint
|
||||||
|
|
||||||
|
The first `entrypoint`, looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
entrypoint = pkgs.writeScript "mediocregopher-navidrome" ''
|
||||||
|
#!${pkgs.bash}/bin/bash
|
||||||
|
exec ${pkgs.navidrome}/bin/navidrome --configfile ${./navidrome.toml}
|
||||||
|
'';
|
||||||
|
```
|
||||||
|
|
||||||
|
The goal here is to provide an executable which can be run directly, and which
|
||||||
|
will put together all necessary environment and configuration (`navidrome.toml`,
|
||||||
|
in this case) needed to run the service. Having the entrypoint split out into
|
||||||
|
its own target, as opposed to inlining it into the service file (defined next),
|
||||||
|
is convenient for testing; it allows you test _exactly_ what's going to happen
|
||||||
|
when running the service normally.
|
||||||
|
|
||||||
|
#### Service
|
||||||
|
|
||||||
|
`service` looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
service = pkgs.writeText "mediocregopher-navidrome-service" ''
|
||||||
|
[Unit]
|
||||||
|
Description=mediocregopher navidrome
|
||||||
|
Requires=network.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1s
|
||||||
|
User=mediocregopher
|
||||||
|
Group=mediocregopher
|
||||||
|
LimitNOFILE=10000
|
||||||
|
|
||||||
|
# The important part!
|
||||||
|
ExecStart=${entrypoint}
|
||||||
|
|
||||||
|
# EXTRA DIRECTIVES ELIDED, SEE
|
||||||
|
# https://www.navidrome.org/docs/installation/pre-built-binaries/
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
'';
|
||||||
|
```
|
||||||
|
|
||||||
|
It's function is to produce a systemd service file. The service file will
|
||||||
|
reference the `entrypoint` which has already been defined, and in general does
|
||||||
|
nothing else.
|
||||||
|
|
||||||
|
#### Install
|
||||||
|
|
||||||
|
`install` looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
install = pkgs.writeScript "mediocregopher-navidrome-install" ''
|
||||||
|
#!${pkgs.bash}/bin/bash
|
||||||
|
sudo cp ${service} /etc/systemd/system/mediocregopher-navidrome.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable mediocregopher-navidrome
|
||||||
|
sudo systemctl restart mediocregopher-navidrome
|
||||||
|
'';
|
||||||
|
```
|
||||||
|
|
||||||
|
This attribute produces a script which will install a systemd service on the
|
||||||
|
system it's run on. Assuming this is done in the context of a functional nix
|
||||||
|
environment and standard systemd installation it will "just work"; all relevant
|
||||||
|
binaries, configuration, etc, will all come along for the ride, and the service
|
||||||
|
will be running _exactly_ what's defined in my repo, everytime. Eat your heart
|
||||||
|
out, ansible!
|
||||||
|
|
||||||
|
Nix is usually used for building things, not _doing_ things, so it may seem
|
||||||
|
unusual for this to be here. But there's a very good reason for it, which I'll
|
||||||
|
get to soon.
|
||||||
|
|
||||||
|
### Makefile
|
||||||
|
|
||||||
|
While `default.nix` _could_ exist alone, and I _could_ just interact with it
|
||||||
|
directly using `nix-build` commands, I don't like to do that. Most of the reason
|
||||||
|
is that I don't want to have to _remember_ the `nix-build` commands I need. So
|
||||||
|
in each directory there's a `Makefile`, which acts as a kind of index of useful
|
||||||
|
commands. The one for navidrome looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
install:
|
||||||
|
$$(nix-build -A install --no-out-link)
|
||||||
|
```
|
||||||
|
|
||||||
|
Yup, that's it. It builds the `install` attribute, and runs the resulting script
|
||||||
|
inline. Easy peasy. Other services might have some other targets, like `init`,
|
||||||
|
which operate the same way but with different script targets.
|
||||||
|
|
||||||
|
## Nix Remotely
|
||||||
|
|
||||||
|
If you were waiting for me to explain _why_ the install target is in
|
||||||
|
`default.nix`, rather than just being in the `Makefile` (which would also make
|
||||||
|
sense), this is the part where I do that.
|
||||||
|
|
||||||
|
My home server isn't the only place where I host services, I also have a remote
|
||||||
|
host which runs some services. These services are defined in this same repo, in
|
||||||
|
essentially the same way as my local services. The only difference is in the
|
||||||
|
`Makefile`. Let's look at an example from my `maddy/Makefile`:
|
||||||
|
|
||||||
|
```
|
||||||
|
install-vultr:
|
||||||
|
nix-build -A install --arg paramsFile ./vultr.nix
|
||||||
|
nix-copy-closure -s ${VULTR} $$(readlink result)
|
||||||
|
ssh -tt -q ${VULTR} $$(readlink result)
|
||||||
|
```
|
||||||
|
|
||||||
|
Vultr is the hosting company I'm renting the server from. Apparently I think I
|
||||||
|
will only ever have one host with them, because I just call it "vultr".
|
||||||
|
|
||||||
|
I'll go through this one line at a time. The first line is essentially the same
|
||||||
|
as the `install` line from my `navidrome` configuration, but with two small
|
||||||
|
differences: it takes in a parameters file containing the configuration
|
||||||
|
specific to the vultr host, and it's only _building_ the install script, not
|
||||||
|
running it.
|
||||||
|
|
||||||
|
The second line is the cool part. My remote host has a working nix environment
|
||||||
|
already, so I can just use `nix-copy-closure` to copy the `install` script to
|
||||||
|
it. Since the `install` script references the service file, which in turn
|
||||||
|
references the `entrypoint`, which in turn references the service binary itself,
|
||||||
|
and all of its configuration, _all_ of it will get synced to the remote host as
|
||||||
|
part of the `nix-copy-closure` command.
|
||||||
|
|
||||||
|
The third line runs the install script remotely. Since `nix-copy-closure`
|
||||||
|
already copied over all possible dependencies of the service, the end result is
|
||||||
|
a systemd service running _exactly_ as it would have if I were running it
|
||||||
|
locally.
|
||||||
|
|
||||||
|
All of this said, it's clear that provisioning this remote host in the first
|
||||||
|
place was pretty simple:
|
||||||
|
|
||||||
|
* Add my ssh key (done automatically by Vultr).
|
||||||
|
* Add my user to sudoers (done automatically by Vultr).
|
||||||
|
* Install single-user nix (two bash commands from
|
||||||
|
[here](https://nixos.wiki/wiki/Nix_Installation_Guide#Stable_Nix)).
|
||||||
|
|
||||||
|
And that's literally it. No docker, no terraform, no kubernubernetes, no yaml
|
||||||
|
files... it all "just works". Will it ever require manual intervention? Yeah,
|
||||||
|
probably... I haven't defined uninstall or stop targets, for instance (though
|
||||||
|
that would be trivial to do). But overall, for a use-case like mine where I
|
||||||
|
don't need a lot, I'm quite happy.
|
||||||
|
|
||||||
|
That's pretty much the post. Hosting services at home isn't very difficult to
|
||||||
|
begin with, and with this pattern those of us who use nix can do so with greater
|
||||||
|
reliability and confidence going forward.
|
||||||
|
|
||||||
|
[flakes]: https://nixos.wiki/wiki/Flakes
|
||||||
|
[home-manager]: https://github.com/nix-community/home-manager
|
||||||
|
[navidrome]: https://github.com/navidrome/navidrome
|
@ -113,6 +113,9 @@ recommendations:
|
|||||||
## Option 3: Twitter
|
## Option 3: Twitter
|
||||||
|
|
||||||
New posts are automatically published to [my Twitter](https://twitter.com/{{
|
New posts are automatically published to [my Twitter](https://twitter.com/{{
|
||||||
site.twitter_username }}). Simply follow me there and pray the algorithm smiles
|
site.twitter_username }}), so you can follow me there and pray the algorithm
|
||||||
upon my tweets enough to show them to you! :pray: :pray: :pray:
|
smiles upon my tweets enough to show them to you! :pray: :pray: :pray:
|
||||||
|
|
||||||
|
(Apparently the twitter algo downranks posts with links in them, so don't waste
|
||||||
|
your time praying too hard.)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user