Flake-based Web Kiosk Project

Hey :wave:

I’m relatively new to Nix, but love the approach excited about what I’m learning!

I just published my first NixOS-based project to GitHub: Avunu/web_kiosk

The idea is to create a trimmed-down USB-booting dedicated web kiosk OS that’s preconfigured to connect to a specified wireless network and open a specified portal page.

Because Firefox 121 builds wayland support by default on Linux and is easily configured in a kiosk mode using a runtime switch, running Firefox in Cage on NixOS compiled to an ISO file made perfect sense for my scenario.

The heart of the project is these two files:

  description = "A NixOS live image for a Firefox web kiosk";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";

  outputs = { self, nixpkgs }:
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
      lib = pkgs.lib;

      envConfig = import ./build.env.nix;
      kioskConfig = import ./kiosk.nix { inherit lib pkgs envConfig; };
      disableConfig = import ./disable.nix;

      nixosConfig = {
        imports = [

      kioskIso = nixpkgs.lib.nixosSystem {
        inherit system;
        modules = [ nixosConfig ];

      packages.x86_64-linux.default = kioskIso.config.system.build.isoImage;


{ lib, pkgs, envConfig, ... }:

  boot.initrd.systemd.network.wait-online.enable = true;
  boot.kernelPackages = pkgs.linuxKernel.packages.linux_zen;
  hardware.enableRedistributableFirmware = true;
  isoImage.isoName = "kiosk.iso";
  isoImage.makeEfiBootable = true;
  isoImage.makeUsbBootable = true;
  isoImage.squashfsCompression = "xz";
  networking.useNetworkd = true;
  networking.wireless = lib.optionalAttrs (envConfig.wifiSSID != "") {
    enable = true;
    networks."${envConfig.wifiSSID}".psk = envConfig.wifiPassword;
  programs.firefox.enable = true;
  services.cage.enable = true;
  services.cage.program = "${pkgs.firefox}/bin/firefox -kiosk ${envConfig.startPage}";
  services.cage.user = "kiosk";
  services.getty.loginProgram = "${pkgs.coreutils}/bin/true";
  system.stateVersion = "23.11";
  systemd.network.enable = true;
  time.timeZone = envConfig.timeZone;
  users.users.kiosk.isNormalUser = true;
  zramSwap.enable = true;

The thing is, I’m aiming for an image that’s as minimal as possible, and flashing a several GBs for a Linux OS with only Cage + Firefox feels really excessive to me.

I tried removing default bloat using the following disable.nix module (I just went gung-ho with finding defaults that didn’t seem necessary and disabling them):

{ lib, ... }:

  appstream.enable = false;
  boot = {
    initrd = {
      services.lvm.enable = false;
      systemd.enableTpm2 = false;
    swraid.enable = lib.mkForce false;
  environment.defaultPackages = [ ];
  environment.systemPackages = [ ];
  fonts.fontconfig.enable = false;
  hardware = {
    bluetooth.enable = false;
    pulseaudio.enable = false;
  programs.nano.enable = false;
  networking = {
    # dhcpcd.enable = false;
    firewall.enable = false;
  nixpkgs.overlays = [
    (final: super: {
      zfs = super.zfs.overrideAttrs (_: {
        meta.platforms = [ ];
  security = {
    pam.services.su.forwardXAuth = lib.mkForce false;
    # polkit.enable = lib.mkForce false;
    sudo.enable = lib.mkForce false;
    tpm2.applyUdevRules = false;
  services = {
    logrotate.enable = lib.mkForce false;
    lvm.enable = false;
    # nscd.enable = false;
    openssh.enable = lib.mkForce false;
    pipewire.enable = false;
    rsyslogd.enable = false;
    syslog-ng.enable = false;
    # udev.enable = false;
    udisks2.enable = false;
    # upower.enable = false;
    xserver.enable = false;
    zfs.trim.enable = lib.mkForce false;
  sound.enable = false;
  system = {
    extraDependencies = lib.mkForce [ ];
    nssModules = lib.mkForce [ ];
    switch.enable = false;
  systemd = {
    coredump.enable = false;
    oomd.enable = false;
    services = {
      systemd-journal-flush.enable = false;
  xdg = {
    autostart.enable = false;
    icons.enable = false;
    menus.enable = false;
    mime.enable = false;
    portal.enable = false;
    sounds.enable = false;

This reduces the image down to ~1.6GB, but I’m convinced it could be trimmed down further. In the world of debian-based OSes, this would probably cost 200-400mb…

Any ideas how to cut some more of the cruft?

Also, aficionado would probably get a kick out of how I’m handling “environment variables”. I tried to set up a private env.nix file where individual users can set their unique configuration variables without tampering with the rest of the git tree or the ability to pull updates. Since .gitignore effectively removes files from the flake build, I can’t include that file in the flake build, so I hackishly copy the local env.nix to an otherwise empty (but tracked) file in build.sh right before building:

#!/usr/bin/env bash

# copy the git ignored variables to the build environment
mv build.env.nix build.env.nix.tmp
mv env.nix build.env.nix

# build the project
nix build --impure --extra-experimental-features 'nix-command flakes'

# restore the git ignored variables
mv build.env.nix env.nix
mv build.env.nix.tmp build.env.nix

If anyone could direct me to a less-hackish approach, that would be great.



I do not have the answers but some maybe useful references.

  • NixNG: A lighter and simpler “NixOS”
  • SrvOS: NixOS for server (maybe smaller defaults)
  • impermanence: ephemeral storage

Less-hackish is a subjective topic.
But you could make your kiosk a flake lib, with a flake template, and a flake commando to build.

That means you could configure nix run github:Avunu/web_kiosk to run nix flake new -t github:Avunu/web_kiosk my-kiosk and user flake to be just an import and kiosko.mk ./env.nix, that also expose the package to nix build.

Flake diagram tax

Hey, a few suggestions: first have you tried to disable documentation and rely on the minimal module like suggested in How to have a minimal NixOS? - #4 by rnhmjoj

You might also be able to save space using a zfs file system with conpression enabled… or lose if zfs is more heavy than what you can compress. You can also maybe try to see what takes the most space by inspecting the closure of the derivation. Another option might be to try to tweak the package options using yourpackage.override {enableFoo = false};. Out of curiosity what makes you thirk that debian would use only 200 Mo?