Skip to content

Repository files navigation

flakelet

Run systemd services from Nix flakes and update them independently of the host system.

Status: beta. Functional and running production services, but the interface may still change.

Normally a NixOS host has to be rebuilt to update one of its services. flakelet takes the same approach as virtualisation.oci-containers, but with flakes instead of container images. The host only declares which flake to run and which settings to pass. The machine itself resolves the flake, evaluates and builds it, and switches the systemd units. There are no images and no registry. The units run straight out of the host's nix store.

Host configuration

{
  inputs.flakelet.url = "github:Mic92/flakelet";

  # in a nixosConfiguration:
  imports = [ flakelet.nixosModules.flakelet ];
  services.flakelets = {
    enable = true;
    services.myservice = {
      flake = "github:example/my-service";   # or prebuilt = <store path>;
      settings = { port = 8080; };           # passed to the service module
      autoUpdate.enable = true;              # periodic re-evaluation
    };
  };
}

Run flakelet update myservice on the machine, or let the generated timer do it. The update evaluates the flake against the host's nixpkgs, builds plain unit files, links them into /run/systemd/system and starts them. Every update becomes a generation with gc roots. If activation or the service's health probe fails, flakelet rolls back to the previous generation.

Secrets never go through settings. Pass paths to host-managed secret files instead, for example from sops-nix, and load them in the unit with LoadCredential=.

Writing a service

$ nix flake init -t github:Mic92/flakelet

A service flake exports an adios-style module: declared options for what the host may pass, and an impl returning the units to run in a typed, NixOS-style interface. Hardening is ordinary systemd configuration such as DynamicUser= and StateDirectory=.

flakelets.default = { types, ... }: {
  options.port = { type = types.number; default = 8000; description = "listen port"; };

  impl = { options, pkgs, name, ... }: {
    services.${name} = {
      # no [Install] section: only started when the socket is hit
      serviceConfig.ExecStart = "${pkgs.myservice}/bin/serve";
      serviceConfig.DynamicUser = true;
    };
    sockets.${name} = {
      socketConfig.ListenStream = options.port;
      wantedBy = [ "sockets.target" ];
    };
  };
};

The host settings are checked against the options before impl is evaluated: unknown keys, wrong types and missing required settings fail the update with the offending name.

Units with an [Install] section (wantedBy) are enabled and started on activation. Units without one are left to systemd's on-demand activation: a socket-activated service starts on the first connection, a timer's job runs on its schedule, not at deploy time. Changed units that are running are restarted either way.

Health lives in the units: Type=notify or ExecStartPost= for readiness, Restart= for liveness. A service can additionally ship a <name>-health.service oneshot (or return healthCheck); flakelet starts it after every activation and rolls back when it fails. The impl can also return exports, free-form metadata like claimed ports or metrics endpoints. flakelet publishes the exports of the running generation to /run/flakelet/exports/<name>.json, where firewall, reverse-proxy, monitoring or backup tooling can pick them up.

The template in templates/service/flake.nix shows all of this. DESIGN.md describes the full contract.

CLI

flakelet update [<name>…]        evaluate, build and activate
flakelet status [--json]         generation, degraded/held state, lock holders
flakelet diff <name>             closure diff: running generation vs. fresh eval
flakelet rollback <name>         previous generation
flakelet remove <name>           stop a service, delete state and generations
flakelet reconcile               remove services dropped from the host config
flakelet lock/unlock <name>      pin to the currently resolved revision
flakelet deploy <name> --flake <ref> --settings s.json    imperative service
flakelet activate <name> <path>  start a prebuilt artifact, no evaluation
flakelet check [--build] [--machine <host>]               CI: evaluate/build off-machine
flakelet build <name>… [--out-link <dir>]                 like check, with result symlinks
flakelet gc [--keep <n>]         prune old generations

The check command also works away from the machine. For example, flakelet check --machine eve --build evaluates the flakelet configuration of nixosConfigurations.eve in the current flake and builds all of its service artifacts. Run it in CI to catch broken services before they reach the machine and to fill the binary cache.

Contracts and providers

Blessed contracts live in contracts/ as JSON Schema, with eval-time constructors in the injected contracts. Known implementations:

Contract Implementation
http/v1 flakelet-nginx
postgres/v1 flakelet-postgres

Real-world examples

Development

$ nix develop
$ cargo test
$ nix build .#checks.x86_64-linux.vm -L   # end-to-end NixOS VM test

Design notes live in DESIGN.md.

About

Run systemd services from Nix flakes and update them independently of the host system

Resources

Stars

17 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages