Zone Configuration
Zones are the primary organisational unit in Nixda Stack. Each zone maps to a VLAN, owns a /24 subnet, and holds a named set of services. This guide covers defining zones, placing services inside them, and using blueprint catalog entries to minimise boilerplate.
For the conceptual background on why zones exist and how they relate to the network topology, see the Architecture page.
Defining a Zone
Add an attribute under nixda.zones with three required fields:
nixda.zones.private = {
vlan = 20;
subnet = "10.100.20";
services = { };
};
| Field | Type | Description |
|---|---|---|
vlan | integer (1–4094) | IEEE 802.1Q tag. Must be unique across all zones. |
subnet | string | First three octets of the zone’s /24 (e.g. "10.100.20"). |
services | attribute set | Named service definitions placed in this zone. |
Zone names (private above) are arbitrary Nix identifiers. They appear in DNS
names ({service}.{zone}.lan) and systemd unit names, so lowercase
alphanumeric strings separated by hyphens are recommended.
VLAN IDs must be globally unique. Nixda Stack does not enforce this at evaluation time; duplicate VLAN IDs will produce a broken network configuration silently.
Placing Services
Add a named attribute under services. At minimum, every service requires id
and exactly one runtime block:
nixda.zones.private.services.myapp = {
id = 10;
ports = [ 8080 ];
runtime.oci = {
image = "myapp:latest";
};
};
Service ID
id is an integer from 1 to 254 (type enforced), though 254 is reserved for the host bridge,
making the usable range 1–253. It determines the service’s static IP address:
IP = zone.subnet + "." + id
For the example above with subnet = "10.100.20" and id = 10, the service
receives the IP 10.100.20.10.
Constraints:
- IDs must be unique within a zone. A NixOS assertion enforces this at evaluation time; duplicate IDs produce a build failure with a message listing the conflicting values.
- ID
254is reserved for the host bridge address (the host’s entry point into the zone). - The valid consumer range is therefore 1–253.
Runtime blocks
Exactly one runtime.* block should be non-null. The four available runtimes
are:
| Block | Execution model |
|---|---|
runtime.oci | Podman container from an OCI image |
runtime.native | NixOS container (systemd-nspawn) |
runtime.system | Host-level systemd service (no isolation) |
runtime.vm | MicroVM with an isolated kernel |
For detailed runtime configuration, see the Native runtime and OCI runtime pages. For lifecycle behavior (idle timeout, socket activation), see Service Lifecycle.
Additional service options
| Option | Type | Default | Description |
|---|---|---|---|
ports | listOf port | [] | TCP ports the service listens on internally |
idleTimeout | nullOr int | 43200 | Inactivity timeout in seconds; null for always-on |
stripPrefix | bool | true | Whether the reverse proxy strips the service-name path prefix |
prefixMapping | attrsOf port | {} | Maps URL path prefixes to specific internal ports |
prefixMapping is defined in the service schema but is not yet wired into
proxy-service.nix. It is reserved for future use — when implemented, it will
allow a single service to route different URL prefixes to different backend ports.
runtime.systemruns directly on the host with no network isolation. The service is reached via127.0.0.1. Itsconfigurationis adeferredModulecomposed manually by the consumer — see the cockpit and terminal blueprints for examples. A dedicated runtime page is not warranted; the implementation is minimal (firewall port opening only).
Using Blueprints
The catalog (config.nixda.catalog.{name}) exposes pre-configured service
attribute sets that match the services submodule type exactly. Merge a
blueprint into a service definition with lib.recursiveUpdate:
nixda.zones.private.services.uptime = lib.recursiveUpdate
config.nixda.catalog.uptime
{
id = 15;
};
lib.recursiveUpdate performs a deep merge. The right-hand side wins on
conflicts. The blueprint provides the runtime defaults (configuration, ports,
idle timeout); the consumer provides:
id— placement within the zone subnet.- Host-specific overrides — any attribute the blueprint explicitly leaves to the consumer.
lib.recursiveUpdatereplaces lists rather than concatenating them. When a blueprint declares volumes and the consumer also declares volumes, the consumer’s list replaces the blueprint’s list entirely. Supply the complete desired list in the consumer stanza.
See the Blueprint Catalog page for the full list of available blueprints and the attributes each one requires the consumer to supply.
Service ID Allocation Reference
| Range | Assignment |
|---|---|
1–253 | Available for service placement |
254 | Reserved — host bridge address |
IP formula: {zone.subnet}.{service.id}
Examples for subnet = "10.100.20":
id | IP |
|---|---|
2 | 10.100.20.2 |
10 | 10.100.20.10 |
100 | 10.100.20.100 |
Multiple Zones Example
A complete two-zone configuration using catalog blueprints:
{ config, lib, ... }:
{
nixda.zones = {
management = {
vlan = 10;
subnet = "10.100.10";
services = {
uptime = lib.recursiveUpdate config.nixda.catalog.uptime { id = 2; };
cockpit = lib.recursiveUpdate config.nixda.catalog.cockpit { id = 3; };
};
};
private = {
vlan = 20;
subnet = "10.100.20";
services = {
syncthing = lib.recursiveUpdate config.nixda.catalog.syncthing {
id = 5;
runtime.native.volumes = [
"/data/syncthing:/var/lib/syncthing"
];
};
};
};
};
}
The resulting IP assignments:
| Zone | Service | IP |
|---|---|---|
management | uptime | 10.100.10.2 |
management | cockpit | 10.100.10.3 |
private | syncthing | 10.100.20.5 |