ubuntu-vanilla-build

Ubuntu Vanilla ISO Builder (No Snap)

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. —

What’s New

Recent improvements include:


What This Build Produces


Supported Ubuntu Targets

Only 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.


Building Pop!_OS ISOs

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).

How it differs from the Ubuntu builder

Desktop note: no pop-desktop / COSMIC option

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.

Installing COSMIC after installation (noble / resolute)

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.)


Building Cloud & VM Disk Images

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.

Image-specific choices (prompted interactively, or via flags)

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.

Examples

# 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 -

Building Removable Media Disk Images

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)

Image-specific choices (prompted interactively, or via flags)

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.

Examples

# 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.


Installing from the Live ISO (CLI Installer)

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).

How to use it

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.squashfs is 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.

What it does not do

For more detail see scripts/cli-installer/README.md.


Build Concepts

Understanding the build pipeline helps with troubleshooting and customization:

Build Pipeline Flow

The build process follows these stages:

  1. Host Setup: Install required tools (debootstrap, squashfs-tools, xorriso) and prepare workspace.
  2. Debootstrap: Create a minimal Ubuntu base system using the chosen release and mirror.
  3. Chroot Configuration: Enter the chroot environment to:
    • Configure APT sources and pinning (including snapd blocking).
    • Install desktop environment and selected packages.
    • Configure browser repositories (Brave, LibreWolf, Firefox).
    • Install Pacstall and optional extras (Ubuntu Studio, etc.).
  4. Live Image Assembly: Create the live filesystem structure with Casper integration.
  5. SquashFS + ISO Creation: Compress the filesystem and generate the bootable ISO with GRUB.
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)

Security Features


Requirements

System Requirements

Host OS Compatibility

The script supports building on:


Quick Start (Beginner Friendly)

Getting Started in 3 Steps

  1. Clone this repository:
    git clone <repository-url>
    cd ubuntu-vanilla-build
    
  2. Run the build script:
    # 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
    
  3. Follow the interactive wizard: On a terminal (TTY), the script first asks which mode to use — press Enter for Basic — then prompts you step-by-step for the configuration.

Interactive Prompts

The script is interactive by default on a TTY when required values are missing. The prompt wizard guides you through choosing:

Prompt defaults match the non-interactive fallbacks below, so pressing Enter everywhere yields the same ISO as an unattended run.

Default Settings

If values are not explicitly set and interactive prompts are skipped, the default fallback settings are:


Command-Line Options

Basic Options

Desktop-Specific Options

Browser Options

Note: Browser repositories are always configured regardless of pre-installation choice, allowing you to easily apt install them post-install.

Additional Options

Locale & Keyboard (Unattended Builds)

Config File & Interactive Mode

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-interactive overrides are advanced-mode features for power users and CI pipelines. When you run build.sh on a terminal without choosing a mode, it first asks whether to run in Basic or Advanced mode (an alternative to passing --advanced).

Build Hooks (Modloader)

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

See scripts/hooks/README.md for detailed documentation and examples.

Advanced Mode

--advanced (or ADVANCED_MODE=1 as an environment variable) unlocks features for power users:

  1. Config file support: Auto-loads 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.
  2. Workspace preservation: On Ctrl+C or build failure, only bind mounts are unmounted — the workspace tree is kept intact. This lets you inspect or resume from a specific build stage (e.g. ./build.sh --advanced run_chroot) without re-running debootstrap.
  3. Package cache: A persistent APT cache directory (~/.cache/ubuntu-vanilla-build/apt-cache/) is bind-mounted into the chroot. Downloaded .deb files survive across builds, saving bandwidth on repeated builds.
  4. Workspace reuse: setup_host reuses an existing workspace directory instead of cleaning it, allowing faster iteration.

Advanced Execution Syntax

For developers who want to control specific build stages:

./build.sh [options] [start_cmd] [-] [end_cmd]

Host commands (run outside chroot):

Examples:


Environment Variables

For advanced configurations, environment variables can be used instead of CLI flags:

Core Build Variables

Desktop-Specific Variables

Browser Variables

Feature & Customization Variables

Locale & Keyboard Variables

Config & Interactive Variables

Advanced Mode Variables


Common Examples

# 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 -

Workspace and Output Behavior

Workspace Directory

The build process uses a workspace directory to store temporary files:

Output Files

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:


Package and Policy Details

Core Policies

Calamares Installer Configuration

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.

Desktop Environment Profiles

Each desktop environment has a carefully curated package set:

Special Features

LXDE Repair Mechanism

For LXDE builds, Calamares includes an automatic repair step:


Verifying Build Artifacts

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

Testing the ISO

Before deploying, test your ISO in a virtual machine:

Creating Bootable USB

Use 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/sdX with your actual USB device (use lsblk to identify). Writing to the wrong device will result in data loss.


Troubleshooting

Common Issues

LXDE Openbox-Only Issue

If you install an LXDE ISO while offline and the system boots to Openbox only:

After you get internet:

  1. Boot into the installed system and select the Openbox session.
  2. Open a terminal and run:
    sudo apt update
    sudo apt install --no-install-recommends lxde
    
  3. Log out or reboot.
  4. At the login screen, select LXDE Session, then log in.

Panel configuration fix: If the taskbar/panel looks wrong or missing:

Advanced Debugging

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

Advanced Customization

Modifying Calamares Configuration

For deep customization of the installer experience, edit the YAML files in scripts/calamares/:

Adding Custom Packages

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.

Creating Custom Desktop Profiles

To add a new desktop environment variant:

  1. Add the desktop name to the validation in normalize_desktop_variant().
  2. Add installation logic in the customize_image() function under a new case statement.
  3. Update the help text in host_help() to document the new option.

Contributing

Contributions are welcome! Areas for contribution:


License

Licensed under GNU General Public License v2.0. See LICENSE.