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:
| Parameter | Derivation | Example |
|---|---|---|
hostBridge | br${zone.vlan} | br20 for VLAN 20 |
localAddress | ${zone.subnet}.${service.id}/24 | 10.100.20.10/24 |
defaultGateway | ${zone.subnet}.254 | 10.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.defaultGatewaynetworking.nameserversnetworking.firewall.allowedTCPPorts(fromservice.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.
| Value | Behavior |
|---|---|
43200 (default) | Container stops after 12 hours of inactivity; starts on next proxied request |
null | Container starts at boot and runs continuously (autoStart = true) |
| Positive integer | Custom 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}.2on the management bridge - DNS resolver pointing to
${management.subnet}.254(host Unbound) - Caddy virtual host at
uptime.management.lanrouting 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.recursiveUpdatemerges attribute sets deeply but replaces lists. Thevolumeslist in the consumer replaces the blueprint’s empty default list entirely.
Reference: runtime.native Options
| Option | Type | Default | Description |
|---|---|---|---|
configuration | deferredModule | — | NixOS configuration evaluated inside the container |
volumes | listOf str | [] | Bind mounts in "host:container" format |
credentials | attrsOf path | {} | Secrets passed via --load-credential; available at /run/credentials/@system/<name> |
credentialEnvFiles | attrsOf { credential, variable } | {} | Generates VARIABLE=value files from raw credentials |
capabilities | listOf str | [] | Additional Linux capabilities for the container |
extraNspawnFlags | listOf str | [] | Additional flags passed directly to systemd-nspawn |
Service-level options that affect native container behavior:
| Option | Type | Default | Description |
|---|---|---|---|
id | int (1–254) | — | Fourth IP octet; must be unique within the zone |
ports | listOf port | [] | TCP ports opened in container firewall and registered with proxy |
idleTimeout | nullOr int | 43200 | Inactivity timeout in seconds; null disables |