Psst, hey kid — ever heard of Nix? Maybe you heard it’s a package manager. Hasn’t one of your neckbeard coworkers grumbled about how they use it as an operating system? “I thought it builds software” you might say.
The truth is it’s all of those things and that can be pretty confusing. I think to understand Nix you have to work your way through understanding how Nix (the language), nixpkgs (the package manager), and NixOS (the operating system) are all Nix.
When I started playing around with Nix (the package manager and the language) I couldn’t grasp how the tool I was using to create development shells for my software projects could also be used to deploy the software that was being developed! Well, the missing piece was Nix (the operating system).
When you assemble the pieces of the Nix puzzle
You can just deploy things.
In my previous post I mentioned the services I’ve deployed on my NixOS-based home server liveoak. One of the benefits of NixOS that I’ve enjoyed is the ease with which you can try out new software. There’s a lot of people using Nix (the operating system) who do the hard
What does this actually look like? Well, even if you don’t know Nix (the language) I hope if you squint at the following code samples hard enough
After a couple of years of hanging out on the orange website I’ve collected a handful of technical blogs which I like to read. Instead of hoping for their posts to end up near the top of the feed, I wanted a way to subscribe to their RSS feeds directly. Since the whole idea of my home server is to… host software for my use… let’s see how we can deploy an RSS reader with Nix.
I’ve heard of one called miniflux, let’s see if it’s available as a NixOS service:
Perfect! Getting this thing running should be as easy as services.miniflux.enable = true;. Getting it configured should be as easy as services.miniflux.config = { a bunch of key/value pairs }. Easy peasy, right? Right.
Here’s what that looks like:
_: {
_class = "clan.service"; # (clan-usage)
roles.server = {
interface =
{ lib, ... }:
{
options = {
port = lib.mkOption { # (port-binding)
type = lib.types.port;
default = 8000;
description = "Port for the Miniflux service.";
};
};
};
perInstance =
{ settings, ... }:
{
nixosModule =
{ pkgs, ... }:
{
imports = [
../../modules/tailscale-serve.nix # (tailscale-import)
];
services.miniflux = {
enable = true; # (miniflux-enable)
config = { # (miniflux-config)
LISTEN_ADDR = "127.0.0.1:${toString settings.port}";
AUTH_PROXY_HEADER = "Tailscale-User-Login";
AUTH_PROXY_USER_CREATION = 1;
CREATE_ADMIN = 0;
TRUSTED_REVERSE_PROXY_NETWORKS = "100.64.0.0/10";
};
adminCredentialsFile = pkgs.writeText "dummy" "DUMMY=1";
};
services.tailscaleServe.miniflux = { # (miniflux-tailscale)
enable = true;
serviceName = "rss";
port = settings.port;
};
};
};
};
}tailscale serve as a oneshot systemd script to expose the service on my tailnet with HTTPS. No port forwarding, no reverse proxy config. Apply that configuration, and boom — miniflux is running on the server accessible on my Tailscale network at https://rss.mytailnet.ts.net.
Pretty sweet. But what just happened? Let’s take a look at what’s going on inside this services.miniflux thing we .enable’ed:
config = mkIf cfg.enable { # (miniflux-enabled)
services.postgresql = lib.mkIf cfg.createDatabaseLocally { # (create-database)
enable = true;
ensureUsers = [
{
name = "miniflux";
ensureDBOwnership = true;
}
];
ensureDatabases = [ "miniflux" ];
};
systemd.services.miniflux = { # (systemd-block)
description = "Miniflux service";
wantedBy = [ "multi-user.target" ];
requires = lib.optional cfg.createDatabaseLocally "miniflux-dbsetup.service";
after = [
"network.target"
]
++ lib.optionals cfg.createDatabaseLocally [
"postgresql.target"
"miniflux-dbsetup.service"
];
serviceConfig = {
Type = "notify";
ExecStart = lib.getExe cfg.package;
User = "miniflux";
};
environment = lib.mapAttrs (_: toString) (lib.filterAttrs (_: v: v != null) cfg.config); # (config-passthru)
};
...
}miniflux.enable option we set to true. true and creates the Postgres database for us. If we wanted to, we could set this to false and manage the database setup ourselves. (source: nixpkgs/nixos/modules/services/web-apps/miniflux.nix)
So, just by .enable = true’ing services.miniflux (and relying on some default behavior) we got a systemd unit wrapping the executable and a configured database. Everything you need to do to “run Miniflux” was done by opting in to services.miniflux.enable. That’s pretty neat, huh?
Let’s Deploy Some(one else’s) Code
One thing that’s fascinating about Nix is that you can just run code from different places. Need to do some “once in a while” operation like resize your VM’s disk partition? Run nix shell nixpkgs#cloud-utils and growpart is available on your PATH. Found a repo on Github that has a flake.nix? How about nix run github:owner/repo#package-name. You can just run the code in your shell, in a systemd unit, in a box, with a fox.
I have a habit of opening a bunch of tabs that “I’ll look at later”. I wanted a proper home for these links that I eventually
Sounds great, this time though we didn’t get so lucky — no one has written a Nix (the operating system) module for us to use.
Oh actually — it’s not packaged in Nix (the package manager) either.
Not a problem at all. If we take a look at the Github repo we can see that the project is written in Rust. Nix (the package manager) already has a lot of packages written in Rust. Like most software projects, the engineers working on it have found high level abstractions to make common patterns easier for other developers.
Let’s take a look at how we can use pkgs.rustPlatform.buildRustPackage and pkgs.fetchFromGithub to deploy someone’s project from Github:
_: {
_class = "clan.service";
roles.server = {
interface =
{ lib, ... }:
{
options = {
port = lib.mkOption {
type = lib.types.port;
default = 8001;
description = "Port for the Feedlynx service.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/feedlynx";
description = "Directory to store the feed file.";
};
};
};
perInstance =
{ instanceName, settings, ... }:
{
nixosModule =
{
config,
pkgs,
lib,
...
}:
let
generatorName = "feedlynx-${instanceName}";
feedlynx = pkgs.rustPlatform.buildRustPackage { # (feedlynx-build)
pname = "feedlynx";
version = "0.4.0";
src = pkgs.fetchFromGitHub { # (feedlynx-fetch)
owner = "wezm";
repo = "feedlynx";
rev = "1f94cc5ba4123829b8cce05ab4c229fbca8c7350";
hash = "sha256-w+ZSFw1DGAb4OQT45/BpVo4AoxKZnwnX/6oZZ8gb9Z0=";
};
cargoHash = "sha256-ZiMWXtDVt/Je8pLyqWCN8SHXG3KzbBHxeacqUBVzWw0=";
};
in
{
imports = [
../../modules/tailscale-serve.nix
];
clan.core.vars.generators.${generatorName} = {
share = false;
files.environmentFile = {
mode = "0400";
};
runtimeInputs = [ pkgs.openssl ];
script = '' # (vars-generator)
PRIVATE_TOKEN=$(openssl rand -hex 32)
FEED_TOKEN=$(openssl rand -hex 32)
cat > $out/environmentFile <<EOF
FEEDLYNX_PRIVATE_TOKEN=$PRIVATE_TOKEN
FEEDLYNX_FEED_TOKEN=$FEED_TOKEN
EOF
'';
};
users.users.feedlynx = {
isSystemUser = true;
group = "feedlynx";
home = settings.dataDir;
createHome = true;
};
users.groups.feedlynx = { };
systemd.services.feedlynx = { # (feedlynx-systemd)
description = "Feedlynx RSS link collector";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
FEEDLYNX_ADDRESS = "127.0.0.1";
FEEDLYNX_PORT = toString settings.port;
};
serviceConfig = {
Type = "simple";
User = "feedlynx";
Group = "feedlynx";
EnvironmentFile = config.clan.core.vars.generators.${generatorName}.files.environmentFile.path;
ExecStart = "${feedlynx}/bin/feedlynx ${settings.dataDir}/feed.xml";
Restart = "on-failure";
RestartSec = 5;
};
};
services.tailscaleServe.feedlynx = {
enable = true;
serviceName = "feedlynx";
port = settings.port;
};
};
};
};
}buildRustPackage is a helper from nixpkgs that knows how to build Rust projects. It handles cargo build, linking, and installing the binary. fetchFromGitHub is a helper from nixpkgs that grabs a specific commit from a repo as the source for buildRustPackage. After evaluating that thing I’ve got Feedlynx running on my server.
I didn’t have to build a Dockerfile and somehow get the image to my server. I didn’t have to muck about with generating secrets for the service. I wrote some Nix (the language) describing how to build and run the service and Nix (the package manager and operating system) figured out how to make that happen for me.
You Can Just Deploy Things
So what is Nix? Nix is a tool that gives you the freedom to try new software, the ability to develop your own software, and to build and deploy any software Cargo.toml, a vibecoded tui you made, or even a SSO solution for your infrastructure.
In an upcoming
If you might find that interesting you can find the RSS feed for my blog below!