You Can Just Deploy Things | Relax, it's software.

9 min read Original article ↗

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. . I used to read this in other blog posts and I never really understood what it meant…

A trinity diagram showing the relationship between Nix, nixpkgs, NixOS, and the Nix DSL

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). . You can evaluate Nix code in a Dockerfile to produce a more traditional build artifact, but that’s a different blog post.

When you assemble the pieces of the Nix puzzle . trinity shield? you feel like you have a superpower when it comes to your relationship with software.

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 . Actually, it can be really easy but there is a high skill cap for more complicated packages work of writing Nix (the language) you need to deploy software with Nix (the package manager).

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 . And read my annotations you’ll still be able to see the interesting bits.

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:

NixOS options search results for services.miniflux showing 10 available options

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;
            };
          };
      };
  };
}
. I use a Nix framework called Clan to manage my machines. . We can let “callers” override the port they want this thing to run on. . Imports the Nix code which is used below to bind this service to my Tailscale network . This is the magic line. One flag and Nix handles installing the package, creating a database, setting up systemd units — everything. . Here are those key/value pairs I promised. . This is a custom module I wrote that wraps 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)
  };
  ...
}
. This corresponds to the miniflux.enable option we set to true. . This option defaults to true and creates the Postgres database for us. If we wanted to, we could set this to false and manage the database setup ourselves. . Pretty important to actually run the software. Nix gives us tools to wrap programs as systemd units. . And here’s where those config key/values actually get used — they just pass through to the systemd unit’s environment.

(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 . 😉 look at. I found feedlynx which can create an RSS feed with links it receives over an HTTP API. Should pair nicely with that miniflux thing we just deployed.

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.

NixOS options search for services.feedlynx showing no options found

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. . Clan makes it easy to generate and set secrets for the systemd unit to consume. . Since there’s no NixOS module for feedlynx, we write our own systemd service definition. It’s more boilerplate than miniflux, but still pretty straightforward.

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 . You can also use nix to cache your software (packages, devshells, etc.) but that’s going to be covered in a future blog post. . That could be a random repo with a Cargo.toml, a vibecoded tui you made, or even a SSO solution for your infrastructure.

In an upcoming . probably the next blog post I’m planning to cover how I’m using Nix to deploy the software and infrastructure for my small business. While learning Nix it was really unclear to me how to do something practical like “run the database migrations for my web app” or “deploy a staging and production branch of my web app”. I’m hoping that giving small practical examples like this can show more examples of what a powerful tool Nix is and maybe get you interested in trying it out!

If you might find that interesting you can find the RSS feed for my blog below!