system.lua Reference
Comprehensive guide to ZoiOS system-wide declarative configuration.
The /etc/zoi/system.lua file is the primary configuration file for ZoiOS. It allows administrators to define the entire state of the operating system, from kernel parameters to user accounts as code.
Core Functions
system(table)
Defines global system metadata and high-level configuration.
| Field | Type | Description |
|---|---|---|
hostname | string | The network hostname for the machine. |
timezone | string | The system timezone (e.g. "UTC", "America/New_York"). |
locale | string | The primary system locale (e.g. "en_US.UTF-8"). |
kernel_params | string | Appended to the kernel command line in the bootloader. |
desktop | string | The desired desktop environment (e.g. "kde", "gnome", "sway"). |
system({
hostname = "parlex-station",
timezone = "America/Los_Angeles",
locale = "en_US.UTF-8",
kernel_params = "quiet loglevel=3 mitigations=off",
})bootloader(table)
Configures the system bootloader.
| Field | Type | Description |
|---|---|---|
type | string | Required. One of "systemd-boot", "grub2", or "limine". |
efi_dir | string | Path to the EFI partition mount point (defaults to "/boot/efi"). |
timeout | number | Boot menu timeout in seconds. |
bootloader({
type = "systemd-boot",
timeout = 5,
})Leaving type unset lets Zoi detect the bootloader from the filesystem, probing
systemd-boot first, then GRUB 2, then Limine.
efi_dir is only consulted for limine, whose entries use ESP-relative paths.
systemd-boot writes to /boot/loader/entries and probes the ESP mount point
(/boot, /boot/efi, /efi) itself, because the entries have to land on the
filesystem the firmware actually reads.
One boot entry is written per kernel, including kernels preserved as fallbacks by
kernel_retention, so several remain selectable after an
update.
packages(table)
Defines the base set of packages installed in the system scope. You can provide a simple list of names or a table for advanced pinning and option selection.
packages({
-- Simple list
"@core/linux",
"@core/systemd",
-- Pinning to a specific version or range
["@main/neovim"] = { version = "^0.10" },
-- Pinning to a channel
["@core/zoi"] = { version = "@stable" },
-- Advanced pinning and options
["@core/base"] = {
options = {
"zoi:@core/systemd",
"zoi:@core/linux",
"zoi:@core/sudo"
}
}
})users(table)
Manages system user accounts.
| Field | Type | Description |
|---|---|---|
password_hash | string | A one-way hash generated by zoi system secret hash. |
groups | list | Supplementary groups for the user. |
shell | string | Path to the login shell (e.g. "/usr/bin/zsh"). |
home | string | Custom path for the home directory. |
users({
zaid = {
password_hash = "$argon2id$v=19$m=65536,t=3,p=4$...",
groups = { "wheel", "video", "docker" },
shell = "/usr/bin/bash",
}
})Accounts are written directly to /etc/passwd, /etc/shadow, /etc/group and
/etc/gshadow. The behaviour worth knowing:
- Idempotent. Re-applying an unchanged
system.luaperforms no writes, sozoi system applyis safe to run on a schedule. - Deterministic. Users are processed in sorted order, so two machines applying the same configuration end up with identical UIDs.
- Existing UIDs are never reassigned. Changing a UID under a populated home directory would orphan every file in it.
- Home directories are created from
/etc/skelwith mode0700. - An account with no
password_hashis locked, not passwordless. It cannot be logged into with a password, but rootsuand SSH key login still work. ZOISEC:values are decrypted automatically, so an encrypted secret works directly aspassword_hash.- A missing group reference warns rather than failing, so one typo does not abort an apply.
groups(table)
Manages system groups.
| Field | Type | Description |
|---|---|---|
gid | number | Explicit Group ID (optional). |
groups({
docker = { gid = 999 }
})Groups are reconciled before users, because a user's supplementary memberships have to resolve to real GIDs.
Accounts the base packages already ship (root, nobody, and so on) are left
alone. system.lua is the source of truth for whether a group exists, not for
every attribute of it, because the packages that own those groups have deliberate
GIDs and memberships that a declarative file would otherwise fight on every apply.
services(table)
Controls system-wide background services.
| Field | Type | Description |
|---|---|---|
enable | boolean | Whether to enable and start the service via systemctl. |
services({
NetworkManager = { enable = true },
sshd = { enable = true },
docker = { enable = true },
})filesystems(list)
Defines the disk layout and mount points for /etc/fstab.
ZoiOS does not automatically partition or format your drives. You must
manually partition and format your target disk before installation. The
filesystems block is used exclusively to generate a valid /etc/fstab for
the target system.
| Field | Type | Description |
|---|---|---|
device | string | Device path or Label (e.g. "/dev/sda1", "LABEL=root"). |
mount | string | The target mount point (e.g. "/", "/home"). |
type | string | Filesystem type (e.g. "btrfs", "ext4", "vfat"). |
options | string | Mount options (e.g. "subvol=@root,compress=zstd:3"). |
filesystems({
{
device = "LABEL=zoios_root",
mount = "/",
type = "btrfs",
options = "subvol=@root,compress=zstd:3,ssd",
},
{
device = "LABEL=zoios_root",
mount = "/home",
type = "btrfs",
options = "subvol=@home,compress=zstd:3",
}
})Applying Changes
To apply your changes to the live system:
sudo zoi system applyZoi will resolve the packages, install them to the store, create a new System Generation, update the FHS view, reconcile the declared users and groups, regenerate /etc/fstab, enable or disable services, run the kernel pipeline, and write bootloader entries.
The apply is idempotent: running it twice with no change in between performs no writes, which is what makes it safe to schedule.
2026 © All Rights Reserved.
- All the content is available under CC BY-SA 4.0, expect where otherwise stated.
- Source code is available on GitLab, licensed under Apache 2.0.
Last updated on
