Chapters

Personal system

This example keeps the system and Home configuration in one file. It provides ZFS storage, a Limine boot menu and a greetd login that opens Mango with Foot, Firefox, Vis and a few basic keybindings.

It assumes an x86_64 machine with UEFI, Secure Boot disabled, an NVMe or SATA disk, Intel graphics supported by i915 or AMD graphics supported by amdgpu, and a wired connection with DHCP.

Configuration

Create hosts/personal/default.nix in your Copland checkout and use the following module. Replace disk with your disk's path from /dev/disk/by-id/ and choose a unique eight-digit hexadecimal networking.hostId for the machine. Adjust the username, timezone and keyboard layout as needed.

Personal system configuration
{ pkgs, ... }:
let
  user = "alice";
  host = "personal";
  disk = "/dev/disk/by-id/REPLACE-WITH-YOUR-DISK";
in
{
  networking = {
    hostName = host;
    hostId = "a1b2c3d4";
  };
  time.timeZone = "Europe/Berlin";
  i18n.defaultLocale = "en_US.UTF-8";
  hardware.console.keyMap = "us";

  boot.supportedFilesystems.zfs.enable = true;
  boot.initrd.kernelModules = [
    "nvme" "ahci" "xhci_pci" "usb_storage" "usbhid" "hid_generic"
    "i915" "amdgpu"
  ];
  hardware.firmware = [ pkgs.linux-firmware ];
  hardware.graphics.enable = true;

  programs.limine = {
    enable = true;
    efiSupport = true;
    efiInstallAsRemovable = true;
  };
  boot.loader.efi.canTouchEfiVariables = false;

  disko.devices = {
    disk.main = {
      type = "disk";
      device = disk;
      content = {
        type = "gpt";
        partitions = {
          ESP = {
            size = "1G";
            type = "EF00";
            content = {
              type = "filesystem";
              format = "vfat";
              mountpoint = "/boot";
              mountOptions = [ "umask=0077" ];
            };
          };
          root = {
            size = "100%";
            content = {
              type = "zfs";
              pool = "rpool";
            };
          };
        };
      };
    };
    zpool.rpool = {
      type = "zpool";
      options.ashift = "12";
      rootFsOptions = {
        compression = "lz4";
        atime = "off";
        mountpoint = "none";
        canmount = "off";
      };
      datasets = {
        root = { type = "zfs_fs"; mountpoint = "/"; };
        nix = { type = "zfs_fs"; mountpoint = "/nix"; };
        home = { type = "zfs_fs"; mountpoint = "/home"; };
      };
    };
  };

  services.dhcpcd.enable = true;
  services.sysklogd.enable = true;
  services.nix-daemon = {
    enable = true;
    settings.experimental-features = [ "nix-command" "flakes" ];
  };
  services.elogind.enable = true;
  services.rtkit.enable = true;
  services.polkit.enable = true;
  programs.pipewire = {
    enable = true;
    alsa.enable = true;
    pulse.enable = true;
    wireplumber.enable = true;
  };
  programs.dconf.enable = true;
  xdg.portal.enable = true;
  services.getty = {
    enable = true;
    ttys.tty2 = { };
  };

  users.users.${user} = {
    isNormalUser = true;
    shell = pkgs.bashInteractive;
    extraGroups = [ "wheel" "video" "render" "input" ];
  };
  programs.doas.enable = true;
  programs.mango.enable = true;
  programs.mango.session.enable = true;
  environment.systemPackages = [ pkgs.git pkgs.curl ];

  services.greetd.enable = true;

  homeEnvironment.users.${user} = {
    enable = true;
    configurationPath = "/home/${user}/copland-os";
    attribute = "hosts.${host}.homes.${user}";
    modules = [
      ({ config, ... }: {
        home.packages = [
          pkgs.foot pkgs.firefox pkgs.vis pkgs.dejavu_fonts
          pkgs.pavucontrol pkgs.wl-clipboard pkgs.xdg-utils
        ];
        home.sessionVariables.EDITOR = "vis";
        services.polkit-agent.enable = true;
        wayland.windowManager.mango.enable = true;
        fonts.fontconfig.enable = true;
        programs.bash = {
          enable = true;
          sourceSessionEnvironment = true;
        };
        xdg.configFile."mango/config.conf".text = ''
          source=${config.xdg.configHome}/mango/environment.conf
          xkb_rules_layout=us
          bind=SUPER,Return,spawn,${pkgs.foot}/bin/foot
          bind=SUPER,b,spawn,${pkgs.firefox}/bin/firefox
          bind=NONE,XF86AudioRaiseVolume,spawn,${pkgs.wireplumber}/bin/wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+
          bind=NONE,XF86AudioLowerVolume,spawn,${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
          bind=NONE,XF86AudioMute,spawn,${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
          bind=SUPER,q,killclient,
          bind=SUPER,j,focusstack,next
          bind=SUPER,k,focusstack,prev
          bind=SUPER+SHIFT,e,quit,
          bind=SUPER,1,view,1
          bind=SUPER,2,view,2
          bind=SUPER+SHIFT,1,tag,1
          bind=SUPER+SHIFT,2,tag,2
        '';
        xdg.configFile."foot/foot.ini".text = ''
          [main]
          font=DejaVu Sans Mono:size=11
          shell=${pkgs.bashInteractive}/bin/bash
        '';
      })
    ];
  };
}

Enabling programs.mango.session.enable registers the integrated Mango session with greetd. The session loads the system and Home environment, creates its D-Bus session and starts the audio prerequisites declared by the PipeWire module. Once Mango opens its sockets, the session enters the single OpenRC user runlevel graphical, which inherits default and includes portals, desktop services and the authentication agent. Logging out returns user services to default and clears the session environment before the session bus exits.

The Home Mango module generates environment.conf, which the compositor configuration sources above. It publishes display variables, including Xwayland's later DISPLAY value, to the shared runtime environment. Enabled modules supply their own variables: the portal module supplies GTK_USE_PORTAL, and a GnuPG agent with SSH support supplies SSH_AUTH_SOCK. No host session script is needed. For a machine that needs a particular GPU, set programs.mango.session.drmDevice to its /dev/dri/by-path/ device path in the host's hardware configuration; otherwise Mango selects the device.

elogind handles the login session and device access. greetd waits for the user's Home activation, so the configuration and user services are already installed when you log in.

The disk layout uses an EFI partition and an unencrypted ZFS pool with separate datasets for /, /nix and /home. See Storage for the distinction between this installation layout and datasets added later for services.

Language and keyboard

i18n.defaultLocale selects the default language and regional formats. Copland builds the required locales automatically. For English application text with German date and number formats, keep en_US.UTF-8 and add i18n.extraLocaleSettings.LC_TIME = "de_DE.UTF-8"; and i18n.extraLocaleSettings.LC_NUMERIC = "de_DE.UTF-8"; to the module.

The keyboard settings are separate. hardware.console.keyMap applies to the text console and login prompt, while xkb_rules_layout in the Mango configuration applies to the desktop. Change both from us to de for a German layout. time.timeZone selects the local timezone.

Audio and desktop services

PipeWire provides audio with WirePlumber managing devices and PulseAudio compatibility enabled by default. Open pavucontrol from a terminal to select speakers, headphones or a microphone and adjust application volumes. The volume and mute keys control the default output.

The desktop portals provide file dialogs and screen sharing for applications that use them. The Polkit agent displays authentication prompts. wl-copy and wl-paste make the Wayland clipboard available from the terminal. Device handling, DNS configuration through DHCP and CA certificates are provided by the selected modules and platform defaults.

Still missing

This example currently uses wired DHCP. Copland does not yet have an integrated WLAN service for joining wireless networks. Automatic clock synchronization is also missing from the module catalog. Setting a timezone does not synchronize the clock.

Registering the host

Add personal = mkHost "personal"; to the existing hosts attribute set in the root default.nix. This makes the single-file configuration available as hosts.personal to the build and deployment commands.

Local installation

Boot a NixOS live environment with ZFS support in UEFI mode. Open a root shell with sudo -i and work from a copy of your Copland checkout containing the configuration above and its host registration.

Build the system and disk preparation script before changing the disk:

system=$(nix --extra-experimental-features "nix-command flakes" build -f . hosts.personal.toplevel --no-link --print-out-paths)
disko=$(nix --extra-experimental-features "nix-command flakes" build -f . hosts.personal.diskoScript --no-link --print-out-paths)

Check the selected disk against lsblk before continuing. The following command erases that disk, creates the configured layout and mounts it under /mnt.

"$disko"

Install the built system and set the password for your user. If you changed user in the configuration, use that name in the password command too.

nixos-install --root /mnt --system "$system" --no-root-passwd --no-channel-copy
nixos-enter --root /mnt -c '/nix/var/nix/profiles/system/sw/bin/passwd alice'

The account has no login password until you set one here. The installation also installs Limine. After these commands succeed, reboot into the installed disk. Copland activates the Home configuration during startup before greetd starts.

First login

Log in as alice at the greetd prompt. Mango starts with these bindings, where Super is usually the Windows key:

BindingAction
Super + ReturnOpen Foot
Super + bOpen Firefox
Super + qClose the focused window
Super + j / kFocus the next / previous window
Super + 1 / 2Switch to tag 1 / 2
Super + Shift + 1 / 2Move the focused window to tag 1 / 2
Super + Shift + eLog out

Open a terminal and use vis path/to/file to edit a file. Vis is also selected through EDITOR for programs that use it.

Keep your checkout, including the new configuration, at /home/alice/copland-os for subsequent changes. This is the path used by the Home commands. From that checkout, ./deploy.sh switch personal local applies system changes and also activates the user's Home configuration. See Switching and Copland Home for the individual commands.

VPS

This example provides a small server with ZFS, static networking, SSH access and a firewall. It uses the QEMU/KVM Virtio device adapter and the VPS ZFS storage profile with legacy BIOS boot. Services are selected explicitly in the host. Check your provider's boot mode and network configuration before using it.

Configuration

Create hosts/vps/default.nix with this module:

VPS configuration
{ lib, pkgs, ... }:
let
  interface = "eth0";
in
{
  devices.qemuVirtioBios.enable = true;
  profiles.storage.vpsZfs.enable = true;
  services.sysklogd.enable = true;
  services.nix-daemon = {
    enable = true;
    settings.experimental-features = [ "nix-command" "flakes" ];
  };
  services.getty = {
    enable = true;
    ttys = {
      tty1 = { };
      ttyS0 = {
        baud = "115200";
        term = "vt102";
      };
    };
  };

  networking = {
    hostName = "vps";
    hostId = "b2c3d4e5";
  };
  time.timeZone = "UTC";
  i18n.defaultLocale = "en_US.UTF-8";
  hardware.console.keyMap = "us";

  disko.devices.disk.main.device = "/dev/vda";
  programs.limine = {
    enable = true;
    efiSupport = false;
    biosSupport = true;
    biosDevice = "/dev/vda";
  };
  fileSystems."/boot".device = lib.mkForce "/dev/vda2";
  zfs.datasets."rpool/state".mountpoint = "/var/lib";

  networking.static = {
    enable = true;
    inherit interface;
    address = "192.0.2.10/24";
    gateway = "192.0.2.1";
  };
  networking.resolver = {
    enable = true;
    nameservers = [ "1.1.1.1" "9.9.9.9" ];
  };

  services.openssh.enable = true;
  users.users.root.openssh.authorizedKeys.keys = [
    "REPLACE-WITH-YOUR-COMPLETE-SSH-PUBLIC-KEY"
  ];
  services.nftables = {
    enable = true;
    interfaces.${interface}.allowedTCPPorts = [ 22 ];
  };

  environment.systemPackages = [ pkgs.vis pkgs.curl pkgs.gitMinimal ];
  environment.variables.EDITOR = "vis";
}

Replace the example address, prefix length, gateway and interface with your provider's values. The 192.0.2.x addresses are placeholders. Choose a unique eight-digit hexadecimal hostId and replace the SSH key placeholder with the complete line from your local public key file, such as ~/.ssh/id_ed25519.pub.

The disk is specified for both Disko and Limine. The /boot partition is the second partition in this profile. If your disk is not /dev/vda, adjust all three paths to match it. Disko creates the root ZFS dataset, and activation adds rpool/state for persistent state under /var/lib. See Storage for additional datasets.

The host explicitly enables the Nix daemon, system logging and console access. Static networking and DNS are enabled separately through networking.static and networking.resolver. For IPv6, add both ipv6Address with its prefix length and ipv6Gateway under networking.static. Static networking expects a directly reachable gateway, so provider-specific routing may need a separate configuration.

SSH and firewall

SSH allows root login with a public key and disables password authentication by default. This gives the installation and deployment scripts the root access they need. Use a key that your local SSH client can also use for the installation connection.

The firewall allows new SSH connections on the selected interface, established connections and the ICMP traffic declared by the module. Outgoing traffic is allowed. Add ports to allowedTCPPorts or allowedUDPPorts when you enable services that should be reachable from outside.

Installation

Add vps = mkHost "vps"; to the existing hosts attribute set in the root default.nix. Run the following commands from your local Copland checkout, replacing 192.0.2.10 with the server's address.

The existing VPS does not need to run NixOS. Installation starts from the current Linux system over root SSH and uses kexec to boot a temporary NixOS installer into memory. The current system must support kexec and provide Bash, curl, sha256sum and tar, with enough memory for the installer. Keep the provider's console or rescue access available while installing. The installation erases the selected disk.

./deploy.sh build vps
./install.sh vps root@192.0.2.10

install.sh builds the system and Disko script locally, uploads and boots the kexec installer, then prepares the disk and installs Copland from that temporary environment. It performs these steps directly without invoking nixos-anywhere. It then reboots and checks that the expected system generation is running. The new SSH key declaration and static network must therefore already be correct for the final connection to work.

If the server is already running a suitable NixOS installer, use ./install.sh vps root@192.0.2.10 --skip-kexec. The script checks for a NixOS installer environment before partitioning.

First login and updates

Connect to the installed system and inspect its services:

ssh root@192.0.2.10
rc-status

After editing the configuration locally, apply an update from your checkout:

./deploy.sh switch vps root@192.0.2.10

The static networking module blocks live switches that change its network settings. For those changes, use ./deploy.sh boot vps root@192.0.2.10 and reboot the server through your existing access. See Switching for generation selection and rollback. Automatic clock synchronization remains an open module gap here too.