Nimi
Nimi is a tiny process manager built for running NixOS modular services in containers and other minimal environments. It turns a NixOS modular services configuration into a reliable, lightweight runtime that starts services, streams logs, and applies predictable restart and startup behavior.
Why Nimi
Modular services are composable Nix modules: you can import a service, override options, and instantiate it multiple times with different settings. Nimi is the runtime that brings those modules to life outside a full init system (i.e. systemd).
If you are new to modular services, the upstream explanation is the best place to start: NixOS Modular Services Manual.
What’s in the box
- A small PID 1 style runtime suitable for containers.
- Clean process execution with structured startup and shutdown flow.
- Configurable restart behavior for resilient services.
- One-time startup hook for quick initialization steps.
- Clear, instance-per-service configuration using modular services.
Usage
- Define services using modular service modules.
- Evaluate the config with
nimi.mkNimiBinto produce JSON. - Run
Nimiwith the generated config to launch and supervise services.
Quick-start
Minimal Nix configuration:
packages.${system}.myNimiWrapper = pkgs.nimi.mkNimiBin {
services."my-service" = {
imports = [ pkgs.some-application.services.default ];
someApplication = {
listen = "0.0.0.0:8080";
dataDir = "/var/lib/my-service";
};
};
settings.restart.mode = "up-to-count";
settings.restart.time = 2000;
}
Run the generated config:
nix run .#myNimiWrapper
Configuration highlights
services: declare named service instances by importing modular service modules and overriding options per instance.settings.restart: choosenever,up-to-count, oralways, and tune delay and retry count.settings.startup: optionally run one binary before services start.settings.logging: write per-service log files; seedocs/logging.md.configData: define per-service config files; seedocs/config-data.md.
Next steps
- Explore service definitions and compose them per environment.
- Use restart policies to match reliability needs.
- Add a startup hook for migrations, warm-ups, or one-time init tasks.
- Create containers with
docs/container.md. - Add
pkgs.nimito an existingnixpkgsinstance withdocs/overlay.md. - Integrate with Nix tooling:
docs/flake-module.md,docs/nixos-module.md, anddocs/home-module.md.
Command Line Interface
The Nimi CLI is the runtime entry-point for a generated modular services config. It validates the config, runs startup hooks, launches services, and streams their logs until shutdown.
Intended use
Nimi is meant to be the final step after evaluating a modular services configuration with nimi.mkNimiBin. It is lightweight enough for containers, but still gives you consistent startup, restart, and shutdown behavior.
Basic flow
- Generate a JSON config using
nimi.mkNimiBin. - Run
nimi --config ./my-config.json validateto check it. - Run
nimi --config ./my-config.json runto launch services.
Commands
validate: read and deserialize the config to ensure it is well-formed.run: start the process manager and run all configured services.
Flags
--config,-c: path to the generated JSON configuration file.
Runtime behavior
- Optional startup binary runs once before services start.
- Each service runs its configured
argv. - Service logs stream to stdout/stderr with the service name as the log target.
- Restart behavior follows
settings.restart(never,up-to-count,always). Ctrl-Ctriggers a graceful shutdown and waits for services to exit.
Example
nimi --config ./result/nimi-config.json validate
nimi --config ./result/nimi-config.json run
Config Data Files
configData lets modular service modules supply config files to a service
instance. Nimi treats the entries in the generated JSON as the final, resolved
files to expose.
At runtime, for each service:
Nimiserializes the service’sconfigDataentries, hashes them, and creates a temp directory (usually under/tmp) namednimi-config-<sha256>.- Each
configData.<name>.sourceis symlinked into that directory at the relativeconfigData.<name>.pathlocation. - The service is started with
XDG_CONFIG_HOMEset to the temp directory, so it can read config files at$XDG_CONFIG_HOME/<path>.
Nimi does not render configData.<name>.text itself; the Nix evaluation/build
step generates the source files and the JSON points at them. Hence, updating the content
requires rebuilding the config and restarting Nimi.
Logging
Nimi streams service logs to stdout/stderr and can also write per-service log
files when settings.logging.enable is set.
File layout
When logging is enabled, Nimi creates a run-specific directory under
settings.logging.logsDir and writes one file per service:
logs-{n}/service-a.txtlogs-{n}/service-b.txt
Where
nis the successive iteration ran using the same logging directory
Each file receives line-oriented output from the service. Both stdout and
stderr are appended to the same file, so the contents reflect the combined
stream.
Configuration
settings.logging = {
enable = true;
logsDir = "my_logs";
};
Notes
- Log files are created at runtime; they do not exist in the Nix store.
- Disabling logging still streams logs to stdout/stderr, but no files are created.
Containers
Nimi ships with a built-in container generator wired through mkContainerImage,
exposed via the package passthru (for example pkgs.nimi.mkContainerImage or
self'.packages.nimi.mkContainerImage in a flake).
It evaluates the same modular services config as mkNimiBin, then builds an OCI
image via nix2container.buildImage with the Nimi runner set as the entrypoint.
Minimal example
pkgs.nimi.mkContainerImage {
services."my-app" = {
imports = [ pkgs.some-application.services.default ];
someApplication.listen = "0.0.0.0:8080";
};
settings.restart.mode = "up-to-count";
};
Build the image:
nix build .#my-container
Image settings
Use settings.container to control the image build. These options map directly
to nix2container.buildImage, so you can pass things like a base image or extra
files.
settings.container = {
name = "my-app";
tag = "v1";
fromImage = inputs.nix2container.packages.${system}.nix2container.pullImage {
imageName = "alpine";
imageDigest = "sha256:...";
finalImageName = "alpine";
finalImageTag = "3.20";
};
copyToRoot = [
(pkgs.buildEnv {
name = "runtime-bins";
paths = [ pkgs.coreutils pkgs.bash ];
pathsToLink = [ "/bin" ];
})
];
};
Notes
- The
entrypointis always the generatedNimirunner frommkNimiBin. settings.containeronly has an effect when building withmkContainerImage.
Overlay
Nimi exposes an overlay as a standalone flake output. Use it when you want to
add nimi to an existing nixpkgs instance in order to make using the passthru attributes easier.
Example
{
inputs.nimi.url = "github:weyl-ai/nimi";
outputs = { self, nixpkgs, nimi, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ nimi.overlays.default ];
};
in
{
packages.${system}.default = pkgs.nimi;
};
}
Flake Module
The flake module output (nimi.flakeModule) wires Nimi into a flake-parts setup.
It takes named service definitions and turns them into runnable Nimi binaries and
container images, so you can build local runners, CI checks, and deployable
artifacts from one source of truth.
What it provides
For each perSystem.nimi.<name> entry, the module generates:
packages.<name>-service: aNimiruntime binary built from the services module.packages.<name>-container: a container image bundling that runtime.checks.<name>-serviceandchecks.<name>-container: the same outputs, wired into CI.
Minimal example
{
perSystem = { pkgs, ... }: {
nimi."web" = {
services."my-app" = {
process.argv = [
(lib.getExe pkgs.my-app)
"--port"
"8080"
];
};
settings.restart.mode = "up-to-count";
settings.restart.time = 2000;
};
};
}
Build or run the outputs:
nix build .#web-service
nix build .#web-container
Notes
- The module is designed for
flake-partsand expectsperSystem.nimientries. - Outputs are generated per system, so each target platform gets its own runner.
NixOS Module
The NixOS module output (nimi.nixosModules.default) wires Nimi into a NixOS
configuration. It takes named service definitions and turns them into system
packages and systemd units, so you can run the same modular service config on
full NixOS.
What it provides
For each nimi.<name> entry, the module generates:
environment.systemPackages: theNimiruntime binary built from the services module.systemd.services.<name>: a system service that runs the generated binary.
Minimal example
{
imports = [
inputs.nimi.nixosModules.default
];
nimi."web" = {
services."my-app" = {
process.argv = [
(lib.getExe pkgs.my-app)
"--port"
"8080"
];
};
settings.restart.mode = "up-to-count";
settings.restart.time = 2000;
};
}
Notes
- Each
nimi.<name>becomes asystemdunit named<name>.service. - The service is configured with a basic restart policy; override in
systemd.servicesif needed.
Home Manager Module
The Home Manager module output (nimi.homeModules.default) wires Nimi into a
Home Manager configuration. It takes named service definitions and turns them
into user packages and systemd user units, so you can run the same modular
service config as per-user services.
What it provides
For each nimi.<name> entry, the module generates:
home.packages: theNimiruntime binary built from the services module.systemd.user.services.<name>: a user service that runs the generated binary.
Minimal example
{
imports = [
inputs.nimi.homeModules.default
];
nimi."web" = {
services."my-app" = {
process.argv = [
(lib.getExe pkgs.my-app)
"--port"
"8080"
];
};
settings.restart.mode = "up-to-count";
settings.restart.time = 2000;
};
}
Notes
- Each
nimi.<name>becomes asystemd --userunit named<name>.service. - User services may require
loginctl enable-lingerif you need them running without an active session.
Nimi library functions
nimi.evalNimiModule
Evaluate a nimi module and return its config. This runs the module set
through lib.evalModules with the nimi module included so you get the
fully merged, validated configuration output.
Example
evalNimiModule {
settings.binName = "my-nimi";
}
Type
evalNimiModule :: AttrSet -> AttrSet
Arguments
- module
- A nimi module attrset.
nimi.toNimiJson
Render an evaluated config to validated JSON. The config is serialized,
formatted with jq, then validated by running nimi --config ... validate
so the resulting file is both pretty-printed and schema-checked.
Example
let cfg = evalNimiModule { settings.binName = "my-nimi"; };
in toNimiJson cfg
Type
toNimiJson :: AttrSet -> Path
Arguments
- evaluatedConfig
- The evaluated nimi config.
nimi.mkNimiBin
Build a wrapper binary for a given nimi module. This evaluates the module,
writes a validated JSON config, and emits a shell wrapper that runs nimi
with that config so consumers can execute it like a normal binary.
Example
mkNimiBin { settings.binName = "my-nimi"; }
Type
mkNimiBin :: AttrSet -> Derivation
Arguments
- module
- A nimi module attrset.
nimi.mkContainerImage
Build a container image for a given nimi module. This evaluates the module,
wires the container entrypoint to the wrapper binary, and uses
nix2container.buildImage when available (otherwise dockerTools.buildImage).
Example
mkContainerImage { settings.binName = "my-nimi"; }
Type
mkContainerImage :: AttrSet -> Derivation
Arguments
- module
- A nimi module attrset.
Options
_module.args
Additional arguments passed to each module in addition to ones
like lib, config,
and pkgs, modulesPath.
This option is also available to all submodules. Submodules do not
inherit args from their parent module, nor do they provide args to
their parent module or sibling submodules. The sole exception to
this is the argument name which is provided by
parent modules to a submodule and contains the attribute name
the submodule is bound to, or a unique generated name if it is
not bound to an attribute.
Some arguments are already passed by default, of which the following cannot be changed with this option:
-
lib: The nixpkgs library. -
config: The results of all options after merging the values from all modules together. -
options: The options declared in all modules. -
specialArgs: ThespecialArgsargument passed toevalModules. -
All attributes of
specialArgsWhereas option values can generally depend on other option values thanks to laziness, this does not apply to
imports, which must be computed statically before anything else.For this reason, callers of the module system can provide
specialArgswhich are available during import resolution.For NixOS,
specialArgsincludesmodulesPath, which allows you to import extra modules from the nixpkgs package tree without having to somehow make the module aware of the location of thenixpkgsor NixOS directories.{ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/minimal.nix") ]; }
For NixOS, the default value for this option includes at least this argument:
pkgs: The nixpkgs package set according to thenixpkgs.pkgsoption.
Type: lazy attribute set of raw value
Declared by:
assertions.*.assertion
Assertion to evaluate and check.
Type: boolean
Declared by:
assertions.*.message
Message to print on assertion failure.
Type: string
Declared by:
meta
meta attributes to
include in the output of generated Nimi packages
Type: lazy attribute set of raw value
Default:
{ }
Example:
{
meta = {
description = "My cool nimi package";
};
}
Declared by:
passthru
passthru attributes to
include in the output of generated Nimi packages
Type: lazy attribute set of raw value
Default:
{ }
Example:
{
passthru = {
doXYZ = pkgs.writeShellApplication {
name = "xyz-doer";
text = ''
xyz
'';
};
};
}
Declared by:
services
Services to run inside the nimi runtime.
Each attribute defines a named modular service: a reusable, composable module that you can import, extend, and tailor for each instance. This gives you clear service boundaries, easy reuse across projects, and a consistent way to describe how each process should run.
The services option is an lazyAttrsOf submodule: the attribute name is
the service name, and the module content defines its behavior. You
typically provide a service by importing a module from a package and
then overriding or extending its options.
For the full upstream explanation and portability model, see the NixOS manual section on Modular Services.
Type: lazy attribute set of (submodule)
Default:
{ }
Example:
{
services."ghostunnel-plain-old" = {
imports = [ pkgs.ghostunnel.services.default ];
ghostunnel = {
listen = "0.0.0.0:443";
cert = "/root/service-cert.pem";
key = "/root/service-key.pem";
disableAuthentication = true;
target = "backend:80";
unsafeTarget = true;
};
};
services."ghostunnel-client-cert" = {
imports = [ pkgs.ghostunnel.services.default ];
ghostunnel = {
listen = "0.0.0.0:1443";
cert = "/root/service-cert.pem";
key = "/root/service-key.pem";
cacert = "/root/ca.pem";
target = "backend:80";
allowCN = [ "client" ];
unsafeTarget = true;
};
};
}
Declared by:
services.<name>.configData
Configuration data files for the service
These files are made available to the service and can be updated without restarting the service process, enabling configuration reloading.
The service manager implementation determines how these files are exposed to the service (e.g., via a specific directory path).
This path is available in the path sub-option for each configData.<name> entry.
This is particularly useful for services that support configuration reloading via signals (e.g., SIGHUP) or which pick up changes automatically, so that no downtime is required in order to reload the service.
Type: lazy attribute set of (submodule)
Default:
{ }
Example:
{
"server.conf" = {
text = ''
port = 8080
workers = 4
'';
};
"ssl/cert.pem" = {
source = ./cert.pem;
};
}
Declared by:
services.<name>.configData.<name>.enable
Whether this configuration file should be generated. This option allows specific configuration files to be disabled.
Type: boolean
Default:
true
Declared by:
services.<name>.configData.<name>.name
Name of the configuration file (relative to the service’s configuration directory). Defaults to the attribute name.
Type: string
Declared by:
services.<name>.configData.<name>.path
The actual path where this configuration file will be available. This is determined by the service manager implementation.
On NixOS it is an absolute path. Other service managers may provide a relative path, in order to be unprivileged and/or relocatable.
Type: string (read only)
Declared by:
services.<name>.configData.<name>.source
Path of the source file.
Type: absolute path
Declared by:
services.<name>.configData.<name>.text
Text content of the configuration file.
Type: null or strings concatenated with “\n”
Default:
null
Declared by:
services.<name>.meta.maintainers
List of maintainers of each module. This option should be defined at most once per module.
The option value is not a list of maintainers, but an attribute set that maps module file names to lists of maintainers.
Type: list of (maintainer)
Default:
[ ]
Example:
[ lib.maintainers.alice lib.maintainers.bob ]
Declared by:
services.<name>.process.argv
Command filename and arguments for starting this service.
This is a raw command-line that should not contain any shell escaping.
If expansion of environmental variables is required then use
a shell script or importas from pkgs.execline.
Type: list of (string or absolute path convertible to it)
Example:
[ (lib.getExe config.package) "--nobackground" ]
Declared by:
services.<name>.services
A collection of modular services that are configured in one go.
You could consider the sub-service relationship to be an ownership relation. It does not automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option.
Type: attribute set of (submodule)
Default:
{ }
Declared by:
settings.binName
Name of the binary to generate with your nimi wrapper.
Changes the name of the default generated binary name from “nimi” to whatever you select.
Type: string
Default:
"nimi"
Example:
{
settings.binName = "my-awesome-service-runner";
}
Declared by:
settings.container
Configures nimi’s builtin container generation.
Note that none of these options will have any effect unless you are using
nimi.mkContainerImage to build your containers.
These are mappings to nix2container’s buildImage function, please
check there for further documentation.
Type: submodule
Default:
{ }
Declared by:
settings.container.copyToRoot
A derivation (or list of derivations) copied in the image root directory
(store path prefixes /nix/store/hash-path are removed,
in order to relocate them at the image /).
pkgs.buildEnv can be used to build a derivation which has to
be copied to the image root. For instance, to get bash and
coreutils in the image /bin:
Type: (list of path in the Nix store) or path in the Nix store convertible to it
Default:
[ ]
Declared by:
settings.container.fromImage
An image that is used as base image of this image;
Use nix2container.pullImage or nix2container.pullImageFromManifest to supply this.
Type: null or path in the Nix store
Default:
null
Declared by:
settings.container.imageConfig
An attribute set describing an image configuration as defined in the OCI image specification.
Type: open submodule of lazy attribute set of anything
Default:
{ }
Declared by:
settings.container.initializeNixDatabase
To initialize the Nix database with all store paths added into the image.
Note this is only useful to run nix commands from the image, for instance to build an image used by a CI to run Nix builds.
Type: boolean
Default:
false
Declared by:
settings.container.layers
A list of layers built with the nix2container.buildLayer function.
If a store path in deps or contents belongs to one of these layers, this store path is skipped.
This is pretty useful to isolate store paths that are often updated from more stable store paths, to speed up build and push time.
Type: list of path in the Nix store
Default:
[ ]
Declared by:
settings.container.maxLayers
The maximum number of layers to create.
This is based on the store path “popularity” as described in this blog post.
Note this is applied on the image layers and not on layers added with the buildImage.layers attribute.
Type: positive integer, meaning >0
Default:
1
Declared by:
settings.container.name
The name of the generated image
Type: string
Default:
"nimi-container"
Declared by:
settings.container.perms
A list of file permisssions which are set when the tar layer is created: these permissions are not written to the Nix store.
Type: list of (submodule)
Default:
[ ]
Declared by:
settings.container.tag
The tag for the generated image to use
Type: string
Default:
"latest"
Declared by:
settings.logging
Logging behavior for the nimi process manager.
This section controls if per-service log files are written during a run. When enabled, each service writes to its own file under the configured logs directory.
Log files are created at runtime and live in a run-specific subdirectory
under logsDir (for example logs-0/service-a.txt). Each line from the
service stdout or stderr is appended to the same file, preserving
execution order as best as possible.
Type: submodule
Default:
{ }
Example:
{
enable = true;
logsDir = "my_logs";
}
Declared by:
settings.logging.enable
Whether to enable If per-service log files should be written to settings.logging.logsDir.
When disabled, log output still streams to stdout/stderr but no files are created. .
Type: boolean
Default:
false
Example:
true
Declared by:
settings.logging.logsDir
Directory to create and write per-service logs to.
Nimi creates a logs-<n> subdirectory inside this path at runtime
and writes one file per service.
Type: string
Default:
"nimi_logs"
Declared by:
settings.restart
Restart policy for the nimi process manager.
Use this to control if and how services are restarted after they exit. This is the main safety net for keeping long-running services alive, and also a guardrail to prevent tight restart loops from burning CPU.
You can choose a policy that matches the reliability needs of each deployment. For development you might disable restarts entirely, while production workloads usually benefit from a bounded or always-on policy.
Type: submodule
Default:
{ }
Example:
{
mode = "up-to-count";
time = 500;
count = 3;
}
Declared by:
settings.restart.count
Maximum number of restart attempts when mode is up-to-count.
Once this limit is reached, the service is left stopped until you intervene or change the configuration.
Type: positive integer, meaning >0
Default:
5
Example:
3
Declared by:
settings.restart.mode
Selects the restart behavior.
never: do not restart failed services.up-to-count: restart up tocounttimes, then stop.always: always restart on failure.
Choose up-to-count if you want a service to get a few retries
during a transient failure, but still fail fast when the issue is
persistent. Choose always when continuous availability matters
more than surfacing the failure.
Type: one of “never”, “up-to-count”, “always”
Default:
"always"
Example:
"up-to-count"
Declared by:
settings.restart.time
Delay between restarts in milliseconds.
Increase this value for crash loops to give the system time to recover resources or for dependent services to come back.
Type: positive integer, meaning >0
Default:
1000
Example:
250
Declared by:
settings.startup
Startup behavior for the nimi process manager.
This section lets you run a one-time initialization command before any configured services are started. It is useful for bootstrapping state, preparing directories, or running a short setup task that should happen once per process manager start.
The command is executed once and then the normal service startup proceeds. If you do not need a startup hook, leave it unset.
Type: submodule
Default:
{ }
Example:
{
runOnStartup = /nix/store/abcd1234-my-init/bin/my-init;
}
Declared by:
settings.startup.runOnStartup
Path to a binary to run once at startup.
This should be a single executable in the Nix store, not a shell
snippet. Use lib.getExe to turn a package or derivation into a
runnable path.
The command runs before services start, so it is a good place to create files, check preconditions, or populate caches. If you need a long-running process, configure it as a service instead.
Set to null to disable.
Type: null or path in the Nix store
Default:
null
Example:
lib.getExe (
pkgs.writeShellApplication {
name = "example-startup-script";
text = ''
echo "hello world"
'';
}
)
Declared by: