Create a bootable Ubuntu live ISO from a minimal base, with a strict no-Snap policy (snapd is pinned with APT priority -1).
This project is designed for:
[!NOTE] This project now is part of jimedrand.org, an new internet-based small organisation and place to develop the open source software that developed by jimed-rand and contributors who’ve helping to working on it. —
Recent improvements include:
scripts/cli-installer/install-system. Useful for installing from a TTY, a minimal/server ISO, or a headless VM console. Mirrors the same welcome → locale → keyboard → partition → users → summary → mount → unpackfs → … → finished sequence as the Calamares module list in scripts/calamares/settings.conf, and offers both TUI (whiptail/dialog) and plain text prompts. See Installing from the Live ISO (CLI Installer)..img files for USB sticks, SD cards, and other removable media — scripts/build-removable.sh (Ubuntu) and scripts/build-popos-removable.sh (Pop!_OS). Choose UEFI-only or Hybrid (BIOS + UEFI) firmware, sparse truncate / fallocate / dd allocation, image size 8/16/32/64 GB or custom, a desktop-ready or CLI/TTY-only profile, and whether to bake user credentials in at build time or create the user after deployment via a first-boot console wizard. Partition layout adapts to image size (8 GB → 2 GB swap, 16 GB+ → 4 GB swap) and growpart resizes root on first boot so a smaller image works on larger media. See Building Removable Media Disk Images. start-here.sh now also offersscripts/build-img.sh (Ubuntu cloud .img), scripts/build-vm.sh (Ubuntu VM image with QCOW2/VDI/VMDK/VHDX exports), and their Pop!_OS counterparts scripts/build-popos-img.sh / scripts/build-popos-vm.sh. Choose UEFI-only or Hybrid (BIOS + UEFI) firmware, netplan/systemd-networkd or NetworkManager, the image allocation tool (sparse truncate, fallocate, or dd), disk size 32/64/128 GB or custom (fixed layout: 512 MB ESP + 4 GB swap + root gets the rest), a desktop-ready or CLI/TTY-only profile, and whether to bake user credentials in at build time or create the user after deployment (cloud-init for cloud images, a first-boot console wizard for VM images). See Building Cloud & VM Disk Images. start-here.sh now also asks which output type to build (--output=iso|img|vm).scripts/build-popos.sh builds a Pop!_OS ISO from the official Pop!_OS APT repositories (staging excluded) — see Building Pop!_OS ISOs. start-here.sh now asks which distro to build (Ubuntu or Pop!_OS).build.sh first asks whether to build in Basic (default, guided prompts) or Advanced mode — an alternative to passing --advanced..sh scripts into scripts/hooks/pre-chroot/ and scripts/hooks/chroot/ to customize the build — scripts run in sorted filename order, like a game modloader.--advanced flag preserves workspace on failure/Ctrl+C for faster re-runs, enables a persistent APT package cache to save bandwidth, and asks where to put the workspace (default ~/uvb-workspace) and the finished ISO (default your home directory)./var/cache/ubuntu-vanilla-build/) that stays out of your way — the same approach used on WSL — and the finished ISO is saved to your home directory (/home/$USER).build.cfg file for repeatable builds (--advanced --config=FILE), or generate one with --generate-config. Basic mode uses interactive prompts.--advanced --no-interactive disables all prompts; combined with --locale and --keyboard-layout for fully unattended builds.ubuntu-24.04-gnome-amd64-260703-041500.iso) so old builds aren’t overwritten.--pacstall / --no-pacstall; installed by default).gnome (default, based on vanilla-gnome-desktop)xfce (Xubuntu-like functional stack, without xubuntu-* branding packages; includes labwc for Wayland)lxde (lightweight LXDE stack with lightdm and slick-greeter, for low-spec systems)lxqt (LXQt via lxqt + sddm + xorg; no lubuntu-desktop / Lubuntu branding metapackages)mate (mate-desktop-environment or lighter mate-desktop-environment-core, plus optional mate-desktop-environment-extras; lightdm + slick-greeter)cinnamon (cinnamon-desktop-environment with lightdm and slick-greeter)budgie (budgie-desktop-environment with lightdm and slick-greeter)kde-plasma (KDE Plasma with selectable APT metapackage: kde-full, kde-standard, or kde-plasma-desktop)calamares (default, all supported releases)ubiquity (jammy only)packages.mozilla.org with pinning)--no-pacstall).UVB_OUTPUT_DIR or the advanced-mode prompt picks another location; default name includes a UTC timestamp):
${TARGET_NAME:-ubuntu-<version>-<desktop>-amd64-<yymmdd-hhmmss>}.iso${TARGET_NAME:-ubuntu-<version>-<desktop>-amd64-<yymmdd-hhmmss>}.iso.sha1${TARGET_NAME:-ubuntu-<version>-<desktop>-amd64-<yymmdd-hhmmss>}.iso.sha256Only these target releases are supported:
| Codename | Version | HWE Suffix |
|---|---|---|
jammy |
22.04 LTS | -hwe-22.04 |
noble |
24.04 LTS | -hwe-24.04 |
resolute |
26.04 LTS | -hwe-26.04 |
Note: Use a host OS that is the same release as the target or newer.
scripts/build-popos.sh is a Pop!_OS variant of build.sh with the same commands, options, and hooks. You can start it directly, or via ./start-here.sh --distro=popos (without --distro, start-here.sh asks interactively).
apt.pop-os.org — debootstrap and the base system use the Ubuntu mirror (https://apt.pop-os.org/ubuntu), and the chroot additionally configures the release, proprietary, and release-ubuntu suites (falling back to apt-origin.pop-os.org per suite only when the CDN does not publish it) — all signed with the Pop!_OS archive keyring. Only the packages the build takes from Pop (base-files, pop-*, the System76 kernel/driver stack) are pinned (o=pop-os-release, priority 1001) — unlike stock Pop!_OS’s repo-wide pin, so the standard Ubuntu desktops this builder installs keep resolving against the Ubuntu archive. Staging repositories are deliberately excluded.jammy (22.04), noble (24.04), and resolute (26.04, released early July 2026).--kernel=system76|generic|lowlatency. The default system76 installs linux-system76 from the Pop!_OS repos (tracks the stable Linux branch); generic/lowlatency keep the stock Ubuntu HWE kernels.--system76-driver (or TARGET_SYSTEM76_DRIVER=1) pre-installs system76-driver from the Pop!_OS repos for System76 machines (fan/keyboard/suspend support). Default is off — the repos stay configured, so it can always be installed later with sudo apt install system76-driver.scripts/calamares-popos/ (Pop!_OS branding); the module set matches scripts/calamares/.workspace-popos/ subdirectory (under the same workspace parent as Ubuntu builds), its own APT cache, and scripts/build-popos.cfg, so Ubuntu and Pop!_OS builds never collide.The desktop choices are the same as the Ubuntu builder (GNOME default, XFCE, KDE Plasma, MATE, Cinnamon, Budgie, LXDE, LXQt). Pop’s own desktops — pop-desktop and COSMIC — are intentionally not offered, because COSMIC still has too many bugs when installed through Calamares.
The installed system already has the Pop!_OS repositories configured, so users who installed from this ISO can add COSMIC themselves:
sudo apt update
sudo apt install cosmic-session
Then log out and pick the COSMIC session from the session menu on the login screen. For the full Pop!_OS desktop stack instead, install pop-desktop. (The build also prints this note at the end of every noble/resolute Pop!_OS build.)
Besides the live-installer ISOs, six builders produce ready-to-use disk images — no installer involved; the image boots straight into a configured system:
| Script | Distro | Output |
|---|---|---|
scripts/build-img.sh |
Ubuntu | Cloud image: raw .img for deployment to cloud VMs |
scripts/build-vm.sh |
Ubuntu | VM image: raw .img + QCOW2 / VDI / VMDK / VHDX exports |
scripts/build-removable.sh |
Ubuntu | Removable-media image: raw .img for USB sticks, SD cards, CF |
scripts/build-popos-img.sh |
Pop!_OS | Cloud image (Pop!_OS repos, staging excluded) |
scripts/build-popos-vm.sh |
Pop!_OS | VM image (Pop!_OS repos, staging excluded) |
scripts/build-popos-removable.sh |
Pop!_OS | Removable-media image (Pop!_OS repos, staging excluded) |
They are copies of build.sh / build-popos.sh: same stages up to run_chroot (same desktops, browsers, Pacstall, hooks, basic/advanced modes, config files), but the final stage is build_disk_image instead of build_iso.
--firmware=uefi|hybrid (default uefi). UEFI-only uses GPT with a 512 MB EFI System Partition. Hybrid boots on both BIOS and UEFI from the same disk: GPT with a 1 MiB BIOS boot partition plus the same 512 MB ESP, with GRUB installed for both the i386-pc and x86_64-efi targets. (bios/legacy are accepted as aliases for hybrid.)--disk-size=32|64|128|<GB> (default 32, minimum 10). Fixed layout: ESP 512 MB + swap 4 GB + root = all remaining space (a 32 GB image leaves ~27.5 GB for root).--alloc-tool=truncate|fallocate|dd (default truncate). How the raw .img file is created on the build host: truncate makes a sparse file (instant; occupies only the data actually written), fallocate preallocates the full size up front, and dd fully zero-writes the file (slowest, maximum compatibility with picky tooling).--profile=desktop|cli. desktop is a desktop-ready image (you pick the desktop environment exactly like the ISO builders); cli is a CLI/TTY-only image with no desktop stack, GUI browsers, or Flatpak.--network=networkd|network-manager. CLI images default to netplan + systemd-networkd but can use NetworkManager (nmcli/nmtui) instead; desktop images always use NetworkManager. On cloud images the choice is also wired into cloud-init’s network renderer.--user-mode=build|deploy:
build bakes a username + password into the image during the build (--username, --password, optional --fullname and --hostname; prompted on a TTY if omitted). On cloud images the baked account also becomes cloud-init’s default user, so provider-injected SSH keys land on it.deploy (cloud default) leaves user creation to cloud-init at deploy time; on VM images it installs a first-boot wizard that asks for username/password/hostname on the VM console the first time it starts.build-vm.sh / build-popos-vm.sh only) — --formats=qcow2,vdi,vmdk,vhdx|all|none (default qcow2; the raw .img is always kept). Conversion is done with qemu-img (qemu-utils is installed on the host automatically).Cloud images always include cloud-init + cloud-guest-utils (growpart), a ttyS0 serial console on the GRUB command line, and default openssh-server to yes. Each image plus its exports get .sha1/.sha256 checksum files, like the ISOs.
# Interactive (asks distro + output type):
./start-here.sh # pick "Cloud image" or "VM image"
./start-here.sh --distro=popos --output=vm
# Unattended Ubuntu cloud image, CLI-only, 64 GB, UEFI, user created by cloud-init:
cd scripts
./build-img.sh --advanced --no-interactive --release=noble --kernel=generic \
--profile=cli --network=networkd --firmware=uefi --disk-size=64 --alloc-tool=truncate -
# Ubuntu VM image with a baked-in account, exported to QCOW2 + VirtualBox VDI:
./build-vm.sh --release=noble --kernel=generic --profile=desktop --desktop=xfce \
--firmware=hybrid --disk-size=32 --user-mode=build --username=alice \
--password='secret' --hostname=xfce-vm --formats=qcow2,vdi -
A pair of builders produces ready-to-flash raw .img files for USB sticks, SD cards, CompactFlash, and any other removable media that BIOS/UEFI can boot from. The same pipeline as the cloud/VM builders (setup_host → debootstrap → run_chroot → build_disk_image) is used, but the final image is laid out so a single .img can be written with dd / balenaEtcher / Rufus and boot directly on whatever computer picks it up.
| Script | Distro | Output |
|---|---|---|
scripts/build-removable.sh |
Ubuntu | Removable-media image: raw .img for USB / SD / CF |
scripts/build-popos-removable.sh |
Pop!_OS | Removable-media image (Pop!_OS repos, staging excluded) |
--firmware=uefi|hybrid (default hybrid). Hybrid boots on both BIOS and UEFI from the same disk: GPT with a 1 MiB BIOS boot partition plus a 512 MB ESP, with GRUB installed for both the i386-pc and x86_64-efi targets. UEFI-only uses GPT with just the 512 MB ESP. (bios / legacy are accepted as aliases for hybrid.)--disk-size=8|16|32|64|<GB> (default 16, minimum 8). The partition layout is computed from the image size:
< 16 GB → 2 GB swap, image ≥ 16 GB → 4 GB swap)growpart expands the root partition to fill any leftover space on the physical stick or card, so flashing a 16 GB image onto a 64 GB stick works as expected./etc/fstab by UUID (no RESUME= so hibernate is not bound to a generic swap UUID).--alloc-tool=truncate|fallocate|dd (default truncate). Same as the cloud/VM builders: truncate makes a sparse file (instant; occupies only the data actually written), fallocate preallocates the full size up front, and dd fully zero-writes the file (slowest, maximum compatibility with picky tooling).--profile=desktop|cli. desktop is a desktop-ready image (you pick the desktop environment exactly like the ISO builder); cli is a CLI/TTY-only image with no desktop stack, GUI browsers, or Flatpak.--network=networkd|network-manager. CLI images default to netplan + systemd-networkd but can use NetworkManager (nmcli/nmtui) instead; desktop images always use NetworkManager.--user-mode=build|deploy:
build bakes a username + password into the image during the build (--username, --password, optional --fullname and --hostname; prompted on a TTY if omitted). cloud-init is told to treat that account as the default user, so any provider-injected metadata still lands on it.deploy (default) leaves user creation to a first-boot console wizard that asks for username / password / hostname on the attached console the first time the image starts.The same cloud-init + cloud-guest-utils packages as the cloud images are installed, but configured for the NoCloud datasource only (datasource_list: [NoCloud, None]) with a tiny meta-data / user-data seed under /var/lib/cloud/seed/nocloud/ so first boot does not stall waiting for EC2/Azure/GCE metadata. Each image plus its checksums get .sha1 / .sha256 files, like the ISOs and cloud/VM images.
# Interactive (asks distro + output type):
./start-here.sh --output=removable # Ubuntu, removable media
./start-here.sh --distro=popos --output=removable # Pop!_OS, removable media
# Unattended Ubuntu removable image, CLI-only, 16 GB, hybrid firmware, baked-in user:
cd scripts
./build-removable.sh --advanced --no-interactive --release=noble --kernel=generic \
--profile=cli --network=networkd --firmware=hybrid --disk-size=16 --alloc-tool=truncate \
--user-mode=build --username=alice --password='secret' --hostname=stick01 -
# Pop!_OS removable image, GNOME desktop, 32 GB, hybrid firmware, first-boot user wizard:
./build-popos-removable.sh --release=noble --profile=desktop --desktop=gnome \
--firmware=hybrid --disk-size=32 --user-mode=deploy -
# Custom 128 GB image with 4 GB swap and dd allocation (max compatibility):
./build-removable.sh --profile=desktop --desktop=kde-plasma --disk-size=128 \
--alloc-tool=dd --user-mode=deploy -
After the build finishes, flash the image to your media:
# Find the device (e.g. /dev/sdb), then:
sudo dd if=ubuntu-noble-cli-removable-amd64-260726-120000.img of=/dev/sdX bs=4M status=progress conv=fsync
# or use balenaEtcher / Rufus / GNOME Disks / KDE Partition Manager.
The first boot takes a few seconds longer than usual while growpart resizes the root partition to fill the rest of the stick and the first-boot wizard (if --user-mode=deploy) prompts on the console.
Once you have built an ISO (Ubuntu or Pop!_OS) and booted a machine from it, the desktop variant launches Calamares automatically. A pure TTY/CLI environment has no desktop, so this project ships a Calamares-equivalent shell installer that walks the user through the same stages from a terminal:
| Step | What it asks | Calamares module it mirrors |
|---|---|---|
| Welcome | Pick TUI (whiptail/dialog) or plain text prompts | welcome |
| Locale | Curated UTF-8 list (default en_US.UTF-8) |
locale + localecfg |
| Keyboard | XKB layout (default us) |
keyboard |
| Timezone | Tries NTP via timedatectl; falls back to the live clock if offline |
locale (useSystemTimezone: true, no GeoIP) |
| Disk | Erase (Calamares GPT layout) or Manual (cfdisk) |
partition |
| User | Full name, username (validated), password (twice, min 8), hostname | users |
| Summary | Review every answer, then confirm | summary |
| Install | Mount → unpack squashfs → machine-id → fstab → locale → keyboard → initramfs touch → users → display manager → network → hwclock → initramfs → grubcfg → GRUB install → packages → umount | full exec: sequence |
| Finished | systemctl -i reboot into the new system |
finished |
Source: scripts/cli-installer/install-system (~1 200 lines, single self-contained bash script, passes bash -n and shellcheck cleanly).
The installer is a single self-contained bash script —
scripts/cli-installer/install-system.
To use it on the live system, make it executable first, then copy it
into the system (e.g. /usr/local/bin/), then run it:
# 1. Make it executable FIRST (a fresh checkout from the repo or a fresh
# copy off the live media is not always +x):
chmod +x scripts/cli-installer/install-system
# 2. Copy the script into the system — install(1) sets mode 0755 in one step:
sudo install -m 0755 scripts/cli-installer/install-system /usr/local/bin/
# 3. Run it as a normal system command:
sudo install-system
If you are iterating on the script and only need it on the live system temporarily, you can skip step 2 and run it in place:
chmod +x scripts/cli-installer/install-system
sudo ./scripts/cli-installer/install-system
The script aborts with a clear error if
/cdrom/casper/filesystem.squashfsis not present — it is designed to run from the live ISO. For a non-live / minimal environment, debootstrap the rootfs manually first, then call the same chroot steps.
cfdisk to set up LUKS by hand if you need it).scripts/calamares/modules/locale.conf), so the timezone step always asks the user.For more detail see scripts/cli-installer/README.md.
Understanding the build pipeline helps with troubleshooting and customization:
scripts/build.sh (your current Ubuntu/Debian system).The build process follows these stages:
debootstrap, squashfs-tools, xorriso) and prepare workspace.graph TD
A[Host: ./build.sh setup_host] -->|Install build dependencies| B[Host: ./build.sh debootstrap]
B -->|Build minimal base rootfs| C[Chroot: ./build.sh run_chroot]
C -->|Apply pins, configure repos, install desktop & browsers| D[Host: ./build.sh build_iso]
D -->|SquashFS compress & assemble bootloader| E(Custom bootable .iso output)
sudo access. Both build scripts (and start-here.sh) validate sudo once up front and keep the credentials alive in the background, so you are not re-prompted for a password mid-build.The script supports building on:
ubuntu-archive-keyring package)git clone <repository-url>
cd ubuntu-vanilla-build
# Method 1: Using the convenience script from repository root
# (asks whether to build Ubuntu or Pop!_OS; or pass --distro=ubuntu|popos)
./start-here.sh -
# Method 2: Directly from scripts directory
cd scripts/
./build.sh - # Ubuntu ISO
./build-popos.sh - # Pop!_OS ISO
The script is interactive by default on a TTY when required values are missing. The prompt wizard guides you through choosing:
--advanced or ADVANCED_MODE was givenjammy, noble, or resolutecalamares or ubiquity (Ubiquity is validated for jammy only)generic or lowlatencygnome, xfce, lxde, lxqt, mate, cinnamon, budgie, or kde-plasmakde-plasma): kde-standard, kde-plasma-desktop, or kde-fullmate): mate-desktop-environment or mate-desktop-environment-core, plus optional MATE extrasrelease (default), origin, or nonePrompt defaults match the non-interactive fallbacks below, so pressing Enter everywhere yields the same ISO as an unattended run.
If values are not explicitly set and interactive prompts are skipped, the default fallback settings are:
calamaresgnomerelease--release=jammy|noble|resolute - Target Ubuntu release.--mirror=URL - Ubuntu package mirror (default: https://archive.ubuntu.com/ubuntu/).--kernel=generic|lowlatency - Kernel flavor.--installer=calamares|ubiquity - Installer type.--desktop=<desktop> - Desktop environment slug.--kde=kde-full|kde-standard|kde-plasma-desktop - KDE package tier.
kde-plasma-desktop: Minimal Plasma desktop.kde-standard: Standard Plasma with common applications (default).kde-full: Complete KDE suite with all applications.--mate=full|core - MATE metapackage choice.
full: Complete MATE desktop (default).core: Lightweight MATE core.--mate-extras / --no-mate-extras - Add MATE extras package.--brave=none|release|origin - Brave browser channel.
release: Stable Brave browser (default).origin: Brave Origin — a minimalist, privacy-focused Brave build without the extra integrated features.none: Skip Brave pre-installation (repo is still configured).--librewolf / --no-librewolf - Pre-install LibreWolf browser.--firefox / --no-firefox - Pre-install Firefox from Mozilla APT.--firefox-esr / --no-firefox-esr - Pre-install Firefox ESR from Mozilla PPA.--thunderbird / --no-thunderbird - Pre-install Thunderbird from Mozilla PPA.Note: Browser repositories are always configured regardless of pre-installation choice, allowing you to easily apt install them post-install.
--ubuntu-studio / --no-ubuntu-studio - Include Ubuntu Studio creative packages.--pacstall / --no-pacstall - Install Pacstall package manager (default: yes).--browser=release|origin - Legacy alias for Brave selection (use --brave instead).--locale=LOCALE - System locale (e.g. en_US.UTF-8) to pre-seed for non-interactive locale configuration.--keyboard-layout=LAYOUT - Keyboard layout code (e.g. us, de, fr).--keyboard-variant=VARIANT - Keyboard variant (e.g. intl, nodeadkeys; optional).--config=FILE - Load build options from a config file (KEY=VALUE format; requires --advanced). If not specified, scripts/build.cfg is loaded automatically when present in advanced mode.--generate-config - Launch an interactive wizard to generate a build.cfg file (advanced mode). Also available from the repository root as ./start-here.sh --create-config (or --generate-config), which first asks which builder (Ubuntu or Pop!_OS) to generate the config for, then forwards --generate-config to it — no sudo or host dependencies needed for this shortcut.--interactive - Force interactive prompts even when stdin is not a TTY (requires --advanced).--no-interactive - Disable all interactive prompts; missing required values use defaults or cause an error (requires --advanced).Note: In basic mode (default), the build uses interactive prompts and sensible defaults — prompts appear whenever a terminal is attached, no flags needed. Config files and the
--interactive/--no-interactiveoverrides are advanced-mode features for power users and CI pipelines. When you runbuild.shon a terminal without choosing a mode, it first asks whether to run in Basic or Advanced mode (an alternative to passing--advanced).
Drop executable .sh scripts into the hooks directories to customize the build pipeline — like a game modloader:
scripts/hooks/
pre-chroot/ # Runs on host after debootstrap, before entering chroot
chroot/ # Runs inside chroot after packages are installed
Scripts are discovered and executed in sorted filename order. Use numeric prefixes to control load order:
hooks/pre-chroot/00-copy-skel-files.sh
hooks/pre-chroot/10-add-custom-repo.sh
hooks/chroot/00-install-extra-packages.sh
hooks/chroot/50-configure-services.sh
TARGET_* environment variables.WORKSPACE_CHROOT (path to the chroot root on the host).|| true for optional operations.--hooks-dir=PATH overrides the default scripts/hooks/ directory.See scripts/hooks/README.md for detailed documentation and examples.
--advanced (or ADVANCED_MODE=1 as an environment variable) unlocks features for power users:
scripts/build.cfg if present, or use --config=FILE for a custom path. Generate one interactively with --generate-config. In basic mode (default), the build relies on interactive prompts instead../build.sh --advanced run_chroot) without re-running debootstrap.~/.cache/ubuntu-vanilla-build/apt-cache/) is bind-mounted into the chroot. Downloaded .deb files survive across builds, saving bandwidth on repeated builds.setup_host reuses an existing workspace directory instead of cleaning it, allowing faster iteration.For developers who want to control specific build stages:
./build.sh [options] [start_cmd] [-] [end_cmd]
Host commands (run outside chroot):
setup_host - Install host dependencies and prepare workspace.debootstrap - Create the minimal base rootfs.run_chroot - Execute the chroot build stages.build_iso - Create SquashFS and generate bootable ISO.Examples:
./build.sh - - Run the entire pipeline (default)../build.sh setup_host - Only run host setup../build.sh setup_host - debootstrap - Run from setup_host through debootstrap../build.sh debootstrap - build_iso - Run from debootstrap through ISO creation.For advanced configurations, environment variables can be used instead of CLI flags:
TARGET_UBUNTU_VERSION - Ubuntu release codename (jammy, noble, resolute).TARGET_UBUNTU_MIRROR - Package mirror URL.TARGET_KERNEL_FLAVOR - Kernel type (generic, lowlatency).TARGET_KERNEL_PACKAGE - Override kernel metapackage name directly.TARGET_INSTALLER - Installer type (calamares, ubiquity).TARGET_DESKTOP - Desktop environment slug.TARGET_KDE_PACKAGE - KDE metapackage when using kde-plasma.TARGET_MATE_PACKAGE - MATE metapackage.TARGET_MATE_EXTRAS - Set to 1 to install MATE extras.TARGET_BRAVE_CHANNEL - Brave channel (none, release, origin).TARGET_BROWSER - Legacy alias for Brave (use TARGET_BRAVE_CHANNEL).TARGET_LIBREWOLF - Set to 1 to pre-install LibreWolf.TARGET_FIREFOX - Set to 1 to pre-install Firefox.TARGET_FIREFOX_ESR - Set to 1 to pre-install Firefox ESR.TARGET_FIREFOX_POPOS - Set to 1 to pre-install Firefox from the Pop!_OS repository (build-popos.sh only). This is the native deb System76 builds from Mozilla source (pop-os/packaging-firefox) — a real browser, not Ubuntu’s snap-transition stub; the build verifies the candidate comes from the Pop!_OS repos, is larger than 10 MB, and has no snapd dependency before installing. Mutually exclusive with TARGET_FIREFOX.TARGET_THUNDERBIRD - Set to 1 to pre-install Thunderbird.TARGET_UBUNTU_STUDIO - Set to 1 to include Ubuntu Studio packages.TARGET_FWUPD - Set to 1 to pre-install fwupd as the last build step (default: 0). Either way, fwupd is banned via an APT pin for the whole build so nothing can pull it in; the pin is removed before the ISO is finalized, so sudo apt install fwupd always works on the installed system.TARGET_OPENSSH_SERVER - Set to 1 to pre-install openssh-server (default: 0). SSH host keys are never shipped in the image: they are wiped at the end of the build and regenerated on first boot by a oneshot systemd unit (ssh-keygen -A), so every installation gets a unique host identity.TARGET_COCKPIT - Set to 1 to pre-install Cockpit from the ${release}-backports pocket for the latest version, as recommended upstream (default: 0). The backports pocket is always configured, so it can also be installed later with sudo apt install -t <release>-backports cockpit.TARGET_GNOME_INSTALL_RECOMMENDS - Set to 1 to install GNOME with recommends.TARGET_PACSTALL - Set to 0 to skip Pacstall installation (default: 1).TARGET_PACKAGE_REMOVE - Space-separated list of packages to remove from target system.TARGET_NAME - Custom output ISO base name (includes UTC timestamp by default).GRUB_LIVEBOOT_LABEL - Custom boot menu entry label.UBUNTU_VANILLA_WORKSPACE - Custom workspace parent directory (overrides the mode defaults: /var/cache/ubuntu-vanilla-build in basic mode, ~/uvb-workspace in advanced mode).UVB_OUTPUT_DIR - Directory where the finished ISO and checksums are placed (default: your home directory).TARGET_LOCALE - System locale for unattended builds (e.g. en_US.UTF-8).TARGET_KEYBOARD_LAYOUT - Keyboard layout code (e.g. us, de).TARGET_KEYBOARD_VARIANT - Keyboard variant (optional, e.g. intl).INTERACTIVE - Set to 0 to disable interactive prompts (equivalent to --no-interactive; advanced mode only).NO_CONFIRM - Set to 1 to skip the pre-build confirmation prompt.NO_COLOR - Set (to any value) to disable colored terminal output; colors are also disabled automatically when stdout is not a TTY or TERM=dumb.ADVANCED_MODE - Set to 1 to preserve workspace on failure/interrupt and enable package cache.HOOKS_DIR - Custom path to the hooks directory (default: scripts/hooks/).# Jammy + generic kernel + defaults
./build.sh --release=jammy --kernel=generic -
# Jammy + Ubiquity installer (jammy only)
./build.sh --release=jammy --kernel=generic --installer=ubiquity -
# Jammy + Calamares + XFCE
./build.sh --release=jammy --kernel=generic --installer=calamares --desktop=xfce -
# Jammy + Calamares + LXDE (lightweight desktop)
./build.sh --release=jammy --kernel=generic --installer=calamares --desktop=lxde -
# Jammy + Calamares + LXQt (SDDM, no Lubuntu branding stack)
./build.sh --release=jammy --kernel=generic --installer=calamares --desktop=lxqt -
# Jammy + Calamares + MATE
./build.sh --release=jammy --kernel=generic --installer=calamares --desktop=mate -
# Jammy + MATE core + extras (non-interactive)
./build.sh --release=jammy --kernel=generic --desktop=mate --mate=core --mate-extras -
# Jammy + Calamares + Cinnamon
./build.sh --release=jammy --kernel=generic --installer=calamares --desktop=cinnamon -
# Jammy + Calamares + Budgie
./build.sh --release=jammy --kernel=generic --installer=calamares --desktop=budgie -
# Jammy + KDE Plasma desktop using the standard package set
./build.sh --release=jammy --kernel=generic --desktop=kde-plasma --kde=kde-standard -
# Noble + KDE Plasma desktop with full KDE package set
./build.sh --release=noble --kernel=generic --desktop=kde-plasma --kde=kde-full -
# Resolute + KDE Plasma desktop with minimal package set
./build.sh --release=resolute --kernel=generic --desktop=kde-plasma --kde=kde-plasma-desktop -
# Jammy + GNOME + LibreWolf and Firefox preinstalled
./build.sh --release=jammy --kernel=generic --desktop=gnome --librewolf --firefox -
# Noble + XFCE + lowlatency kernel
./build.sh --release=noble --kernel=lowlatency --desktop=xfce -
# Noble + GNOME + Brave Origin preinstalled
./build.sh --release=noble --kernel=generic --desktop=gnome --brave=origin -
# Noble + no Brave, but preinstall Firefox
./build.sh --release=noble --kernel=generic --brave=none --firefox -
# Noble + GNOME with recommends enabled
TARGET_GNOME_INSTALL_RECOMMENDS=1 ./build.sh --release=noble --kernel=generic --desktop=gnome -
# Noble + GNOME + Ubuntu Studio extras
./build.sh --release=noble --kernel=generic --desktop=gnome --ubuntu-studio -
# Noble + custom mirror
./build.sh --release=noble --kernel=generic --mirror=http://archive.ubuntu.com/ubuntu/ -
# Resolute + generic kernel + defaults
./build.sh --release=resolute --kernel=generic -
# Resolute + XFCE + lowlatency kernel
./build.sh --release=resolute --kernel=lowlatency --desktop=xfce -
# Resolute + GNOME + Brave skipped + LibreWolf preinstalled
./build.sh --release=resolute --kernel=generic --desktop=gnome --brave=none --librewolf -
# Resolute + GNOME + Brave Origin + Firefox preinstalled
./build.sh --release=resolute --kernel=generic --desktop=gnome --brave=origin --firefox -
# Resolute + GNOME with recommends enabled
TARGET_GNOME_INSTALL_RECOMMENDS=1 ./build.sh --release=resolute --kernel=generic --desktop=gnome -
# Advanced mode: generate a config file with the wizard
./build.sh --generate-config
# ...or from the repository root (asks Ubuntu or Pop!_OS first):
# ./start-here.sh --create-config
# Advanced mode: build using a config file
./build.sh --advanced --config=build.cfg -
# Advanced mode: preserve workspace for faster re-runs
./build.sh --advanced --release=noble --kernel=generic -
The build process uses a workspace directory to store temporary files:
/var/cache/ubuntu-vanilla-build/workspace/ — a root-owned system directory kept out of the user’s way (contains chroot/ and image/ subdirectories during build). Because it always lives on a Linux-native filesystem, WSL /mnt/c unpack issues can’t occur.~/uvb-workspace; the actual workspace becomes <answer>/workspace). Non-interactive advanced runs use the default silently.UBUNTU_VANILLA_WORKSPACE=/some/path to use a custom parent directory (actual workspace becomes /some/path/workspace) and skip the prompt./mnt/..., /media/..., 9p/DrvFs), it is automatically relocated to the system directory above, because debootstrap cannot unpack reliably there.--advanced, the workspace is preserved on failure or Ctrl+C (only bind mounts are unmounted). This enables faster re-runs: debootstrap is skipped automatically when the chroot already exists, so you can jump straight to the failed stage.After a successful build, the following files are placed in your home directory (/home/$USER) by default. In advanced mode you are asked interactively where to put them (default: your home directory), and UVB_OUTPUT_DIR=/some/path overrides the location in any mode. Files are chowned back to the invoking user, and the default name carries a UTC timestamp so repeat builds never overwrite each other:
${TARGET_NAME:-ubuntu-<version>-<desktop>-amd64-<yymmdd-hhmmss>}.iso - The bootable ISO image.${TARGET_NAME:-ubuntu-<version>-<desktop>-amd64-<yymmdd-hhmmss>}.iso.sha1 - SHA-1 checksum for verification.${TARGET_NAME:-ubuntu-<version>-<desktop>-amd64-<yymmdd-hhmmss>}.iso.sha256 - SHA-256 checksum for verification.snapd is blocked via APT pinning (Pin-Priority: -1) to ensure a snap-free Ubuntu experience. The Snap app-store backends (gnome-software-plugin-snap, plasma-discover-backend-snap) are pinned out too; the Flatpak backends (gnome-software-plugin-flatpak, plasma-discover-backend-flatpak) are installed instead.fwupd is pinned out for the entire build so no package can pull it in. Unlike the snapd pin this one is build-time only — it is dropped before the ISO is finalized, and TARGET_FWUPD=1 / --fwupd pre-installs fwupd on request.The build uses Calamares configuration from scripts/calamares/. Edit YAML files in this directory to modify installer flow, partitioning defaults, welcome screen, locale behavior, and post-install package removals.
Each desktop environment has a carefully curated package set:
vanilla-gnome-desktop with optional recommends (lighter by default).xubuntu-* branding packages; includes labwc for Wayland sessions.lxde, xorg, lightdm, and slick-greeter for low-spec systems.lxqt, sddm, and xorg (no Lubuntu branding).xorg, lightdm, and slick-greeter; optional extras available.xorg, lightdm, and slick-greeter.xorg, lightdm, and slick-greeter.kde-plasma-desktop, kde-standard, or kde-full); SDDM uses Plasma’s own Breeze theme, installed explicitly and pinned via /etc/sddm.conf.d; Slick SDDM packages (any name combining slick and sddm) are excluded with an APT pin (Pin-Priority: -1), and any non-Breeze sddm-theme-* package pulled in by a metapackage is purged, so third-party themes cannot take over the login screen; Discover ships with the Flatpak backend (the Snap backend is blocked).--no-pacstall or TARGET_PACSTALL=0.TARGET_KERNEL_PACKAGE).For LXDE builds, Calamares includes an automatic repair step:
apt-get update && apt-get install --no-install-recommends lxde in the target root.After building, verify the integrity of your ISO using the provided checksum files (replace the name with your actual timestamped ISO):
cd scripts/
sha256sum -c ubuntu-24.04-gnome-amd64-<yymmdd-hhmmss>.iso.sha256
sha1sum -c ubuntu-24.04-gnome-amd64-<yymmdd-hhmmss>.iso.sha1
Before deploying, test your ISO in a virtual machine:
qemu-system-x86_64 -cdrom <your-iso-file>.iso -m 4096 -enable-kvmUse a tool like dd, balenaEtcher, or Rufus (Windows) to create a bootable USB:
# Using dd (Linux/macOS)
sudo dd if=<your-iso-file>.iso of=/dev/sdX bs=4M status=progress conv=fsync
[!WARNING] Replace
/dev/sdXwith your actual USB device (uselsblkto identify). Writing to the wrong device will result in data loss.
--release, --kernel, and any toggles).--installer=ubiquity only with --release=jammy (22.04 LTS).ubuntu-archive-keyring package: sudo apt install ubuntu-archive-keyringUBUNTU_VANILLA_WORKSPACE (or the advanced-mode prompt) at /mnt/..., it is relocated automatically to /var/cache/ubuntu-vanilla-build.If you install an LXDE ISO while offline and the system boots to Openbox only:
After you get internet:
sudo apt update
sudo apt install --no-install-recommends lxde
Panel configuration fix: If the taskbar/panel looks wrong or missing:
For detailed debugging, you can run individual build stages:
# Run only host setup to check dependencies
./build.sh setup_host
# Run debootstrap stage to check base system creation
./build.sh debootstrap
# Run chroot stage to check package installation
./build.sh run_chroot
For deep customization of the installer experience, edit the YAML files in scripts/calamares/:
settings.conf - Main Calamares configuration and module sequence.modules/ - Individual module configurations (partitioning, users, packages, etc.).branding/ - Visual branding and slideshow content.i18n/SUPPORTED - Supported locales.The recommended way to add custom packages is with build hooks (see Build Hooks). Drop a script into scripts/hooks/chroot/ that runs apt-get install -y <your-packages> — no need to modify build.sh at all.
Alternatively, for permanent changes to the build pipeline itself, modify the customize_image() function in scripts/build.sh. Look for the desktop-specific sections and add your packages to the appropriate apt-get install commands.
To add a new desktop environment variant:
normalize_desktop_variant().customize_image() function under a new case statement.host_help() to document the new option.Contributions are welcome! Areas for contribution:
Licensed under GNU General Public License v2.0. See LICENSE.