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.
Sandbox
Nimi provides a lightweight sandbox runner via mkBwrap. It uses
bubblewrap to run your services in
an isolated environment without requiring container runtimes like Docker or
Podman.
What it provides
- Isolated filesystem: A tmpfs-based root with selective host paths bound read-only.
- Environment variables: Set via
settings.bubblewrap.environment. - Working directory: Set via
settings.bubblewrap.chdir. - Namespace isolation: Separate user, PID, UTS, IPC, and cgroup namespaces by default.
- Writable directories:
/tmp,/run,/var,/etcare tmpfs mounts for runtime writes. - Nix store access:
/nix/storeis bind-mounted read-only so binaries can access their dependencies.
Minimal example
pkgs.nimi.mkBwrap {
services."my-app" = {
process.argv = [ (lib.getExe pkgs.my-app) ];
};
settings.bubblewrap = {
environment.MY_VAR = "value";
chdir = "/app";
extraTmpfs = [ "/data" ];
};
}
Run the sandbox:
nix run .#my-sandbox
Extended example
pkgs.nimi.mkBwrap {
services."web-server" = {
process.argv = [ (lib.getExe pkgs.nginx) "-g" "daemon off;" ];
};
settings.bubblewrap = {
environment = {
APP_ENV = "production";
LOG_LEVEL = "info";
};
chdir = "/srv";
roBinds = [
{ src = "/nix/store"; dest = "/nix/store"; }
{ src = "/etc/ssl"; dest = "/etc/ssl"; }
{ src = "/run/secrets"; dest = "/secrets"; }
];
extraTmpfs = [ "/var/cache/nginx" ];
};
}
How it works
- Build time: The nimi binary is wrapped in a shell script that invokes
bwrap. - Execution:
bwrapruns the nimi binary inside an isolated namespace with:- Tmpfs mounts created first (providing writable areas)
- Read-only bind mounts layered on top (e.g.,
/nix/storeover/nix) /devand/procbound from the host- Environment variables and working directory applied
- Namespaces unshared according to configuration
Differences from containers
| Feature | mkBwrap | mkContainerImage |
|---|---|---|
| Runtime dependencies | bubblewrap only | Container runtime (Docker, Podman) |
| Image format | None (uses Nix store directly) | OCI image |
| Startup time | Fast (no image loading) | Depends on runtime |
| Portability | Linux only | Any OCI-compatible runtime |
| Base images | Not supported | Supported via fromImage |
| Root filesystem | Tmpfs with bind mounts | Layered image filesystem |
Notes
- The sandbox requires Linux with user namespaces enabled.
- Writes go to tmpfs directories; they are lost when the sandbox exits.
- Signal handling (Ctrl+C) is supported.
- The entrypoint is always the generated nimi runner from
mkNimiBin. - Not available on macOS (
meta.badPlatformsincludes Darwin).
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.mkNimiBinWithConfig
Build a wrapper binary from an already-evaluated nimi config. This 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.
Use this when you already have an evaluated config (e.g., from evalNimiModule)
and want to avoid re-evaluating the module.
Example
let cfg = evalNimiModule { settings.binName = "my-nimi"; };
in mkNimiBinWithConfig cfg
Type
mkNimiBinWithConfig :: AttrSet -> Derivation
Arguments
- evaluatedConfig
- An already-evaluated nimi config (output of
evalNimiModule).
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.
This is a convenience wrapper around mkNimiBinWithConfig that handles
module evaluation for you.
Example
mkNimiBin { settings.binName = "my-nimi"; }
Type
mkNimiBin :: AttrSet -> Derivation
Arguments
- module
- A nimi module attrset.
nimi.mkContainerImageWithConfig
Build a container image from an already-evaluated nimi config. This wires
the container entrypoint to the wrapper binary and uses
nix2container.buildImage when available (otherwise dockerTools.buildImage).
Use this when you already have an evaluated config (e.g., from evalNimiModule)
and want to avoid re-evaluating the module.
Example
let cfg = evalNimiModule { settings.binName = "my-nimi"; };
in mkContainerImageWithConfig cfg
Type
mkContainerImageWithConfig :: AttrSet -> Derivation
Arguments
- evaluatedConfig
- An already-evaluated nimi config (output of
evalNimiModule).
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).
This is a convenience wrapper around mkContainerImageWithConfig that handles
module evaluation for you.
Example
mkContainerImage { settings.binName = "my-nimi"; }
Type
mkContainerImage :: AttrSet -> Derivation
Arguments
- module
- A nimi module attrset.
nimi.mkBwrapWithConfig
Build a sandboxed wrapper using bubblewrap from an already-evaluated nimi config.
This creates a nimi binary and wraps it in a bubblewrap sandbox configured via
settings.bubblewrap options.
Use this when you already have an evaluated config (e.g., from evalNimiModule)
and want to avoid re-evaluating the module.
The sandbox is configured through the module system with sensible defaults:
/nix/storeand/sysare read-only bound/etc/resolv.confis bound with--ro-bind-try(skipped if missing)/nix,/tmp,/run,/var,/etcare tmpfs mounts/devand/procare bound- Network is shared but user/pid/uts/ipc/cgroup namespaces are unshared
Example
let cfg = evalNimiModule {
settings.binName = "my-sandboxed-app";
settings.bubblewrap.unshare.pid = true;
};
in mkBwrapWithConfig cfg
Type
mkBwrapWithConfig :: AttrSet -> Derivation
Arguments
- evaluatedConfig
- An already-evaluated nimi config (output of
evalNimiModule).
nimi.mkBwrap
Build a sandboxed wrapper using bubblewrap for a given nimi module.
This evaluates the module, creates a nimi binary, and wraps it in a
bubblewrap sandbox configured via settings.bubblewrap options.
This is a convenience wrapper around mkBwrapWithConfig that handles
module evaluation for you.
The sandbox is configured through the module system with sensible defaults:
/nix/storeand/sysare read-only bound/etc/resolv.confis bound with--ro-bind-try(skipped if missing)/nix,/tmp,/run,/var,/etcare tmpfs mounts/devand/procare bound- Network is shared but user/pid/uts/ipc/cgroup namespaces are unshared
Example
mkBwrap {
settings.binName = "my-sandboxed-app";
settings.bubblewrap = {
environment.MY_VAR = "value";
roBinds = [
{ src = "/nix/store"; dest = "/nix/store"; }
{ src = "/data"; dest = "/data"; }
];
tmpfs = [ "/tmp" "/run" ];
chdir = "/app";
unshare.pid = true;
};
}
Type
mkBwrap :: AttrSet -> Derivation
Arguments
- module
- A nimi module attrset. Configure the sandbox via
settings.bubblewrap.
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.bubblewrap
Sandbox configuration for running nimi inside bubblewrap.
Use this to isolate the nimi process manager and its services from the host system. Bubblewrap provides lightweight containerization through Linux namespaces without requiring root privileges or a container runtime.
The defaults provide a minimal sandbox that can access the Nix store and network while isolating the process namespace, filesystem writes, and other system resources. Adjust these settings based on what your services actually need.
Note that these options only take effect when using nimi.mkBwrap to
build your sandboxed binary.
Type: submodule
Default:
{ }
Example:
{
environment.APP_ENV = "production";
chdir = "/app";
roBinds = [
{ src = "/nix/store"; dest = "/nix/store"; }
{ src = "/etc/ssl"; dest = "/etc/ssl"; }
];
extraTmpfs = [ "/app/cache" ];
}
Declared by:
settings.bubblewrap.appendFlags
Extra flags appended after the generated bubblewrap arguments.
Use this for one-off bwrap options not covered by the module.
These appear at the end of the command line, just before --.
Type: list of string
Default:
[ ]
Example:
[ "--cap-add" "CAP_NET_BIND_SERVICE" ]
Declared by:
settings.bubblewrap.bind.dev
Whether to enable bind /dev into the sandbox.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.bind.proc
Whether to enable bind /proc into the sandbox.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.chdir
Working directory to change to after entering the sandbox.
Set this to control where services start. When null, bubblewrap
does not change directory and the process inherits the caller’s
working directory (usually /).
Type: null or string
Default:
null
Example:
"/app"
Declared by:
settings.bubblewrap.devBinds
Device bind mounts from the host into the sandbox.
Each entry maps a host path (src) to a path inside the sandbox
(dest). Unlike regular bind mounts, device bind mounts allow
access to device nodes, making them suitable for binding paths
like /dev/dri for GPU access or /dev/snd for audio.
The sandbox can access device files at these paths. Use this when you need to expose specific device nodes to sandboxed processes.
For paths that may not exist on all systems, use tryDevBinds
instead.
Type: list of (submodule)
Default:
[ ]
Example:
[
{ src = "/dev/dri"; dest = "/dev/dri"; }
{ src = "/dev/snd"; dest = "/dev/snd"; }
]
Declared by:
settings.bubblewrap.devBinds.*.dest
Path inside the sandbox where src appears.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.devBinds.*.src
Host path to bind into the sandbox.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.dieWithParent
Whether to enable terminate sandbox when parent process exits.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.environment
Environment variables to set inside the sandbox.
These are passed to bubblewrap via --setenv and are available to
the nimi process manager and all services it spawns.
Type: lazy attribute set of string
Default:
{ }
Example:
{
APP_ENV = "production";
LOG_LEVEL = "info";
}
Declared by:
settings.bubblewrap.extraTmpfs
Additional tmpfs mounts appended to the default list.
Use this to add writable scratch directories without replacing
the standard set. For complete control, override tmpfs directly.
Type: list of string
Default:
[ ]
Example:
[ "/app/cache" "/app/tmp" ]
Declared by:
settings.bubblewrap.flags
Final list of flags passed to the bwrap executable.
This is computed automatically from the other options in this
module. You can read it to inspect the generated command line, but
setting it directly replaces the generated flags entirely and may
break the sandbox. Prefer prependFlags or appendFlags to inject
custom arguments.
Type: list of string
Default:
[ ]
Declared by:
settings.bubblewrap.gid
Use a custom group id in the sandbox.
Only applies if unshare.user is true.
Type: null or 32 bit unsigned integer; between 0 and 4294967295 (both inclusive)
Default:
null
Example:
1000
Declared by:
settings.bubblewrap.hostname
Use a custom hostname in the sandbox.
Only applies if unshare.uts is true.
Type: null or string
Default:
null
Example:
nixos
Declared by:
settings.bubblewrap.prependFlags
Extra flags inserted before the generated bubblewrap arguments.
Use this when argument order matters, for example to set options that must appear early in the bwrap invocation.
Type: list of string
Default:
[ ]
Example:
[ "--clearenv" ]
Declared by:
settings.bubblewrap.roBinds
Read-only bind mounts from the host into the sandbox.
Each entry maps a host path (src) to a path inside the sandbox
(dest). The sandbox can read these paths but not modify them.
The default includes /nix/store (required for Nix binaries) and
/sys (for system information). Override this list carefully;
omitting /nix/store will break most Nix-built programs.
For paths that may not exist on all systems, use tryRoBinds
instead.
Type: list of (submodule)
Default:
[
{
dest = "/nix/store";
src = "/nix/store";
}
{
dest = "/sys";
src = "/sys";
}
]
Example:
[
{ src = "/nix/store"; dest = "/nix/store"; }
{ src = "/etc/ssl"; dest = "/etc/ssl"; }
{ src = "/run/secrets"; dest = "/secrets"; }
]
Declared by:
settings.bubblewrap.roBinds.*.dest
Path inside the sandbox where src appears.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.roBinds.*.src
Host path to bind into the sandbox.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.tmpfs
Paths to mount as temporary filesystems inside the sandbox.
These mounts are writable but ephemeral; contents are lost when the sandbox exits. They also hide any host content at the same path.
The default list creates writable areas for common system paths
while keeping the sandbox isolated. The /nix tmpfs is mounted
first, then /nix/store is bind-mounted on top, allowing writes
elsewhere under /nix (like /nix/var) without touching the
real store.
Type: list of string
Default:
[
"/nix"
"/tmp"
"/run"
"/var"
"/etc"
]
Example:
[ "/tmp" "/run" ]
Declared by:
settings.bubblewrap.tryDevBinds
Device bind mounts that are skipped if the source does not exist.
Like devBinds, but uses --dev-bind-try which silently skips
the mount if the host path does not exist. Use this for device
paths that may not be present on all systems, such as GPU or
audio devices.
Type: list of (submodule)
Default:
[ ]
Example:
[
{ src = "/dev/dri"; dest = "/dev/dri"; }
{ src = "/dev/nvidia0"; dest = "/dev/nvidia0"; }
]
Declared by:
settings.bubblewrap.tryDevBinds.*.dest
Path inside the sandbox where src appears.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.tryDevBinds.*.src
Host path to bind into the sandbox.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.tryRoBinds
Read-only bind mounts that are skipped if the source does not exist.
Like roBinds, but uses --ro-bind-try which silently skips the
mount if the host path does not exist. Use this for paths that may
not be present on all systems.
The default includes /etc/resolv.conf for DNS resolution, which
may not exist on systems using systemd-resolved or other DNS
configurations.
Type: list of (submodule)
Default:
[
{
dest = "/etc/resolv.conf";
src = "/etc/resolv.conf";
}
]
Example:
[
{ src = "/etc/resolv.conf"; dest = "/etc/resolv.conf"; }
{ src = "/etc/hosts"; dest = "/etc/hosts"; }
]
Declared by:
settings.bubblewrap.tryRoBinds.*.dest
Path inside the sandbox where src appears.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.tryRoBinds.*.src
Host path to bind into the sandbox.
Type: string
Example:
"/etc/resolv.conf"
Declared by:
settings.bubblewrap.uid
Use a custom user id in the sandbox.
Only applies if unshare.user is true.
Type: null or 32 bit unsigned integer; between 0 and 4294967295 (both inclusive)
Default:
null
Example:
1000
Declared by:
settings.bubblewrap.unshare.cgroup
Whether to enable create a new cgroup namespace.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.unshare.ipc
Whether to enable create a new IPC namespace.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.unshare.pid
Whether to enable create a new PID namespace.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.unshare.user
Whether to enable create a new user namespace.
Type: boolean
Default:
true
Example:
true
Declared by:
settings.bubblewrap.unshare.uts
Whether to enable create a new UTS (hostname) namespace.
Type: boolean
Default:
true
Example:
true
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:
{
WorkingDir = {
_type = "override";
content = "/root";
priority = 1000;
};
}
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: