Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Define services using modular service modules.
  2. Evaluate the config with nimi.mkNimiBin to produce JSON.
  3. Run Nimi with 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: choose never, up-to-count, or always, and tune delay and retry count.
  • settings.startup: optionally run one binary before services start.
  • settings.logging: write per-service log files; see docs/logging.md.
  • configData: define per-service config files; see docs/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.nimi to an existing nixpkgs instance with docs/overlay.md.
  • Integrate with Nix tooling: docs/flake-module.md, docs/nixos-module.md, and docs/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

  1. Generate a JSON config using nimi.mkNimiBin.
  2. Run nimi --config ./my-config.json validate to check it.
  3. Run nimi --config ./my-config.json run to 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-C triggers 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:

  • Nimi serializes the service’s configData entries, hashes them, and creates a temp directory (usually under /tmp) named nimi-config-<sha256>.
  • Each configData.<name>.source is symlinked into that directory at the relative configData.<name>.path location.
  • The service is started with XDG_CONFIG_HOME set 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.txt
  • logs-{n}/service-b.txt

Where n is 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 entrypoint is always the generated Nimi runner from mkNimiBin.
  • settings.container only has an effect when building with mkContainerImage.

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, /etc are tmpfs mounts for runtime writes.
  • Nix store access: /nix/store is 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

  1. Build time: The nimi binary is wrapped in a shell script that invokes bwrap.
  2. Execution: bwrap runs 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/store over /nix)
    • /dev and /proc bound from the host
    • Environment variables and working directory applied
    • Namespaces unshared according to configuration

Differences from containers

FeaturemkBwrapmkContainerImage
Runtime dependenciesbubblewrap onlyContainer runtime (Docker, Podman)
Image formatNone (uses Nix store directly)OCI image
Startup timeFast (no image loading)Depends on runtime
PortabilityLinux onlyAny OCI-compatible runtime
Base imagesNot supportedSupported via fromImage
Root filesystemTmpfs with bind mountsLayered 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.badPlatforms includes 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: a Nimi runtime binary built from the services module.
  • packages.<name>-container: a container image bundling that runtime.
  • checks.<name>-service and checks.<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-parts and expects perSystem.nimi entries.
  • 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: the Nimi runtime 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 a systemd unit named <name>.service.
  • The service is configured with a basic restart policy; override in systemd.services if 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: the Nimi runtime 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 a systemd --user unit named <name>.service.
  • User services may require loginctl enable-linger if 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/store and /sys are read-only bound
  • /etc/resolv.conf is bound with --ro-bind-try (skipped if missing)
  • /nix, /tmp, /run, /var, /etc are tmpfs mounts
  • /dev and /proc are 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/store and /sys are read-only bound
  • /etc/resolv.conf is bound with --ro-bind-try (skipped if missing)
  • /nix, /tmp, /run, /var, /etc are tmpfs mounts
  • /dev and /proc are 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: The specialArgs argument passed to evalModules.

  • All attributes of specialArgs

    Whereas 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 specialArgs which are available during import resolution.

    For NixOS, specialArgs includes modulesPath, which allows you to import extra modules from the nixpkgs package tree without having to somehow make the module aware of the location of the nixpkgs or 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 the nixpkgs.pkgs option.

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 to count times, 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: