Native Runtime (nspawn)

Overview

The native runtime creates a NixOS container for each service using systemd-nspawn. Each container runs a full NixOS configuration, gets a private network namespace, and attaches to the zone’s Linux bridge with a static IP derived from the service ID.

Use this runtime when the service is available in nixpkgs (or as a NixOS module from a flake input) and you want declarative, reproducible configuration without an OCI image dependency.


Defining a Native Service

Set runtime.native on the service definition. The required field is configuration; volumes is optional.

nixda.zones.private.services.myapp = {
  id = 10;
  ports = [ 3000 ];
  runtime.native = {
    configuration = { pkgs, ... }: {
      # Full NixOS configuration
      services.myapp.enable = true;
      services.myapp.port = 3000;
    };
    volumes = [
      "/data/myapp:/var/lib/myapp"
    ];
  };
};

Exactly one runtime namespace (native, oci, system, or vm) must be set per service. The others default to null and are ignored.


Container Networking

The module (modules/service/native-service.nix) translates the service definition into a containers.<name> entry. The networking parameters are derived automatically:

ParameterDerivationExample
hostBridgebr${zone.vlan}br20 for VLAN 20
localAddress${zone.subnet}.${service.id}/2410.100.20.10/24
defaultGateway${zone.subnet}.25410.100.20.254
nameservers[ gateway ][ "10.100.20.254" ]

privateNetwork = true isolates the container in its own network namespace. The container reaches the host’s Unbound resolver via the host bridge address (.254), which resolves .lan domains for all other services.

Declared ports are opened in the container’s firewall (networking.firewall.allowedTCPPorts). They are not published to the host — traffic reaches the container via the bridge IP.


The configuration Field

configuration accepts a deferredModule — the same type as a NixOS module, written as a function that receives { pkgs, config, lib, ... }. It is evaluated as part of the container’s NixOS configuration alongside the network settings the module injects.

You can use any NixOS option available in a standard NixOS container:

configuration = { pkgs, lib, ... }: {
  services.postgresql = {
    enable = true;
    package = pkgs.postgresql_16;
    initialScript = pkgs.writeText "init.sql" ''
      CREATE DATABASE myapp;
    '';
  };

  users.users.myapp = {
    isSystemUser = true;
    group = "myapp";
  };
  users.groups.myapp = {};
};

The container automatically receives:

  • system.stateVersion = "24.11"
  • networking.defaultGateway
  • networking.nameservers
  • networking.firewall.allowedTCPPorts (from service.ports)

Do not redeclare these in configuration; they are set by the runtime module.

If the service requires a flake input (for example, a NixOS module not in nixpkgs), the container receives specialArgs = { inherit inputs; }, making all flake inputs available inside configuration.


Volumes

volumes is a list of bind mount strings in "host_path:container_path" format. Each entry maps a directory on the NixOS host into the container as a writable bind mount.

volumes = [
  "/data/syncthing:/var/lib/syncthing"
  "/media/shared:/mnt/shared"
];

The container path becomes the key in containers.<name>.bindMounts. All mounts are isReadOnly = false.

Without a volume for stateful directories, data written inside the container does not persist across container restarts. Services that write to /var/lib/<name> must have a volume mount to a persistent host path.


Credentials

credentials passes secrets into the container via systemd-nspawn’s --load-credential mechanism. Each entry maps a credential name to a host path. Symlinks are resolved. Inside the container, credentials are available at /run/credentials/@system/<name>.

runtime.native = {
  credentials = {
    db_password = "/run/secrets/db_password";
  };
  configuration = { ... }: {
    # Read credential inside container
    # cat /run/credentials/@system/db_password
  };
};

When a NixOS module expects an EnvironmentFile (e.g., KEY=value format) rather than a raw credential, use credentialEnvFiles to convert:

runtime.native = {
  credentials = {
    api_token = "/run/secrets/api_token";
  };
  credentialEnvFiles = {
    api_env = {
      credential = "api_token";
      variable = "API_TOKEN";
    };
  };
  configuration = { ... }: {
    systemd.services.myapp.serviceConfig.EnvironmentFile = "/run/credentials-env/api_env";
  };
};

Each credentialEnvFiles entry generates /run/credentials-env/<name> containing VARIABLE=<value>. A systemd oneshot service (credential-env-files) runs at boot to create these files.


Container Privileges

By default, systemd-nspawn containers run with a restricted capability set and seccomp filter. Services that run nested container runtimes (e.g., Docker inside the container) require elevated privileges.

capabilities adds Linux capabilities to the container via --capability=. extraNspawnFlags passes arbitrary flags to systemd-nspawn.

runtime.native = {
  # Required for Docker-in-nspawn
  capabilities = [ "CAP_SYS_ADMIN" ];
  extraNspawnFlags = [ "--system-call-filter=bpf" ];

  configuration = { ... }: {
    virtualisation.docker.enable = true;
  };
};

Both settings are needed for Docker-in-nspawn because capabilities and seccomp are independent security layers: CAP_SYS_ADMIN grants permission for cgroup and BPF operations, while --system-call-filter=bpf adds the bpf() syscall to the seccomp allowlist.


Idle Timeout

The idleTimeout option (defined at the service level, not inside runtime.native) controls whether the container participates in socket-activated on-demand lifecycle management.

ValueBehavior
43200 (default)Container stops after 12 hours of inactivity; starts on next proxied request
nullContainer starts at boot and runs continuously (autoStart = true)
Positive integerCustom inactivity timeout in seconds

When idleTimeout is set to a non-null value, the container has autoStart = false in the generated configuration and is managed by the proxy socket activation chain.

To keep a blueprint-configured service always-on:

nixda.zones.private.services.uptime = lib.recursiveUpdate
  config.nixda.catalog.uptime
  {
    id = 10;
    idleTimeout = null;
  };

For the full socket activation flow and systemd unit relationships, see Service Lifecycle. For zone placement and ID allocation, see Zone Configuration.

Example: Uptime Kuma via Blueprint

The uptime blueprint encapsulates the full native container configuration for uptime-kuma. The consumer supplies only id and zone placement:

nixda.zones.management.services.uptime = lib.recursiveUpdate
  config.nixda.catalog.uptime
  { id = 2; };

The blueprint provides port 3001, services.uptime-kuma.enable = true, binding on 0.0.0.0:3001 inside the container, and the default 12-hour idle timeout.

The generated container will have:

  • Static IP ${management.subnet}.2 on the management bridge
  • DNS resolver pointing to ${management.subnet}.254 (host Unbound)
  • Caddy virtual host at uptime.management.lan routing to the socket activation proxy

Example: Always-On Syncthing

The syncthing blueprint sets idleTimeout = null because stopping a Syncthing node breaks the P2P sync mesh. The consumer must provide a persistent volume:

nixda.zones.private.services.syncthing = lib.recursiveUpdate
  config.nixda.catalog.syncthing
  {
    id = 25;
    runtime.native.volumes = [
      "/data/syncthing:/var/lib/syncthing"
    ];
  };

lib.recursiveUpdate merges attribute sets deeply but replaces lists. The volumes list in the consumer replaces the blueprint’s empty default list entirely.


Reference: runtime.native Options

OptionTypeDefaultDescription
configurationdeferredModuleNixOS configuration evaluated inside the container
volumeslistOf str[]Bind mounts in "host:container" format
credentialsattrsOf path{}Secrets passed via --load-credential; available at /run/credentials/@system/<name>
credentialEnvFilesattrsOf { credential, variable }{}Generates VARIABLE=value files from raw credentials
capabilitieslistOf str[]Additional Linux capabilities for the container
extraNspawnFlagslistOf str[]Additional flags passed directly to systemd-nspawn

Service-level options that affect native container behavior:

OptionTypeDefaultDescription
idint (1–254)Fourth IP octet; must be unique within the zone
portslistOf port[]TCP ports opened in container firewall and registered with proxy
idleTimeoutnullOr int43200Inactivity timeout in seconds; null disables