{"id":45579211,"url":"https://github.com/usrz/minimal-ubuntu","last_synced_at":"2026-02-23T11:32:00.710Z","repository":{"id":76582493,"uuid":"375328949","full_name":"usrz/minimal-ubuntu","owner":"usrz","description":"Minimal AMI setup for Ubuntu","archived":false,"fork":false,"pushed_at":"2025-02-28T00:41:55.000Z","size":118,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-02-28T05:21:13.667Z","etag":null,"topics":["ami","aws","aws-ec2","ubuntu"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/usrz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-09T11:20:27.000Z","updated_at":"2025-02-28T00:39:52.000Z","dependencies_parsed_at":"2025-02-27T21:38:25.597Z","dependency_job_id":"2d67e08b-9781-47fe-b3d6-5e79dd9d6faf","html_url":"https://github.com/usrz/minimal-ubuntu","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/usrz/minimal-ubuntu","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/usrz%2Fminimal-ubuntu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/usrz%2Fminimal-ubuntu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/usrz%2Fminimal-ubuntu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/usrz%2Fminimal-ubuntu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/usrz","download_url":"https://codeload.github.com/usrz/minimal-ubuntu/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/usrz%2Fminimal-ubuntu/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29741588,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-23T07:44:07.782Z","status":"ssl_error","status_checked_at":"2026-02-23T07:44:07.432Z","response_time":90,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ami","aws","aws-ec2","ubuntu"],"created_at":"2026-02-23T11:31:58.835Z","updated_at":"2026-02-23T11:32:00.700Z","avatar_url":"https://github.com/usrz.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"Installing a minimal version of Ubuntu\n======================================\n\nThis guide will walk you through the process of installing a minimal version of\nUbuntu 24.04 (Noble Numbat) on various types of systems that support UEFI\n(including creating AMIs for EC2 instances) or Raspberry Pis.\n\n* [Required packages](#required-packages)\n* [Preparing the volume](#preparing-the-volume)\n  * [Normal volume](#normal-volume)\n  * [Disk image](#disk-image)\n  * [EC2 volume](#ec2-volume)\n* [Partitioning the volume](#partitioning-the-volume)\n  * [GPT and UEFI](#gpt-and-uefi)\n  * [MSDOS (for Raspberry Pi)](#msdos-for-raspberry-pi)\n* [Formatting and mounting](#formatting-and-mounting)\n  * [Mounting partitions for UEFI systems](#mounting-partitions-for-uefi-systems)\n  * [Mounting partitions for Raspberry Pi](#mounting-partitions-for-raspberry-pi)\n* [Architecture and repository URL](#architecture-and-repository-url)\n  * [Ubuntu repositories](#ubuntu-repositories)\n  * [AWS repositories](#aws-repositories)\n* [Bootstrapping the system](#bootstrapping-the-system)\n* [Operating system installation](#operating-system-installation)\n* [Bonus Packages](#bonus-packages)\n  * [NodeJS 22.x](#nodejs-22x)\n  * [Tailscale](#tailscale)\n  * [Cloudflare](#cloudflare)\n* [User login](#user-login)\n* [Kernel and helper packages](#kernel-and-helper-packages)\n  * [AWS EC2 kernel](#aws-ec2-kernel)\n  * [Raspberry Pi kernel](#raspberry-pi-kernel)\n  * [Other systems](#other-systems)\n* [Cleaning up](#cleaning-up)\n\nRequired packages\n=================\n\nBefore we begin, it's important to make sure you have the necessary packages\ninstalled. This guide requires the use of:\n\n* `parted` to partition volumes\n* `dedebootstrap` to bootstrap the operationg system\n* `zerofree` to optionally clean up volumes after installation.\n\nYou can install these packages by running the following command:\n\n```shell\napt-get update \u0026\u0026 apt-get install --yes parted debootstrap zerofree\n```\n\n\n\nPreparing the volume\n====================\n\nRegardless of what kind of system you're trying to install, you will need an\nempty volume to install Ubuntu. You will need to set the root volume's device\nas the `BASE_DEV` environment variable.\n\n\n### Normal volume\n\nIf targeting a normal device you can simply export the `BASE_DEV` environment\nvariable directly:\n\n```shell\n# Simply export the root device upon which we want to install the OS\nexport BASE_DEV=/dev/sdb\n```\n\n\n### Disk image\n\nIf you want to create an image to be later written to an SD card or USB stick,\n(think Raspberry Pi) you need to first create the image file:\n\n```shell\n# Create a simple image file, 4Gb will suffice for this install\ndd if=\"/dev/zero\" of=\"/raspberry-os.img\" bs=4M count=1024 conv=fsync status=progress\n```\n\nThen you can use the loopback interface to use the image as a normal disk:\n\n```shell\n# Find our first available \"loopback\" device and use it as our BASE_DEV\nBASE_DEV=\"$(losetup -f)\"\n\n# Associate our image file with the loopback device\nlosetup -P \"${BASE_DEV}\" \"/raspberry-os.img\"\n```\n\n\n### EC2 Volume\n\nFrom the EC2 console, create a new volume (4GiB should suffice) and attach it\nto a running EC2 instance where we can run the installation process.\n\nYour mileage might vary, but simply export the device name (in most cases\n`/dev/nvme1n1`) as the `BASE_DEV` environment variable.\n\n```shell\n# Simply export the root device upon which we want to install the OS\nexport BASE_DEV=/dev/nvme1n1\n```\n\n\n\nPartitioning the volume\n=======================\n\nWe will create two partitions on the disk: a small (256 MiB) FAT32 partition\nfor `boot` and the rest of the space as our EXT4 `root` volume.\n\nYou can use `parted` to simply create a basic partition layout.\n\n\n### GPT and UEFI\n\n```shell\n# Create the GPT partition table\nparted -s \"${BASE_DEV}\" mklabel gpt\n\n# Create our two BOOT and ROOT partitions\nparted -s \"${BASE_DEV}\" mkpart UEFI 1MiB 256MiB\nparted -s \"${BASE_DEV}\" mkpart ROOT 256MiB 100%\n\n# Setup flags on our UEFI partition\nparted -s \"${BASE_DEV}\" set 1 boot on\nparted -s \"${BASE_DEV}\" set 1 esp on\n```\n\n\n### MSDOS (for Raspberry Pi)\n\n```shell\n# Create the MSDOS partition table\nparted -s \"${BASE_DEV}\" mklabel msdos\n\n# Create our two BOOT and ROOT partitions\nparted -s \"${BASE_DEV}\" mkpart primary fat32 1MiB 256MiB\nparted -s \"${BASE_DEV}\" mkpart primary ext4 256MiB 100%\n\n# Set the boot partition as bootable\nparted -s \"${BASE_DEV}\" set 1 boot on\n```\n\n\n\nFormatting and mounting\n=======================\n\nBefore moving on to the next step, it's important to give the system a moment\nto populate the device tree. This may take a second or two.\n\nOnce the kernel has finished re-populating the device tree, we can proceed to\nidentify the UUIDs and devices of our partitions. To do this, we will use the\nfollowing commands:\n\n```shell\n# Find the UUID and device of the boot partition\nexport BOOT_UUID=\"$(partx -go UUID -n 1 \"${BASE_DEV}\")\"\nexport BOOT_DEV=\"$(realpath /dev/disk/by-partuuid/\"${BOOT_UUID}\")\"\n\n# Find the UUID and device of the root partition\nexport ROOT_UUID=\"$(partx -go UUID -n 2 \"${BASE_DEV}\")\"\nexport ROOT_DEV=\"$(realpath /dev/disk/by-partuuid/\"${ROOT_UUID}\")\"\n```\n\nWith our partition UUIDs and devices identified, we can now format and mount\nour file systems.\n\nWe'll need to format the `boot` partition as `FAT32` and the `root` partition\nas `EXT4`. To do so, use the following command:\n\n```shell\nmkfs.fat -n \"UBUNTU_BOOT\" -F 32 \"${BOOT_DEV}\"\nmkfs.ext4 -F \"${ROOT_DEV}\"\n```\n\n\n### Mounting partitions for UEFI systems\n\nFor UEFI systems we'll mount the root partition under `/mnt` and the boot\npartition will be mounted under `/mnt/boot/efi`:\n\n```shell\nmount \"${ROOT_DEV}\" /mnt\nmkdir -p /mnt/boot/efi\nmount  \"${BOOT_DEV}\" /mnt/boot/efi\n```\n\n\n### Mounting partitions for Raspberry Pi\n\nOn the other hand, for the Raspberry Pi, we'll mount similarly the root\npartition under `/mnt` and, but the mountpoint of our boot partition is\n`/mnt/boot/firmware`:\n\n```shell\nmount \"${ROOT_DEV}\" /mnt\nmkdir -p /mnt/boot/firmware\nmount  \"${BOOT_DEV}\" /mnt/boot/firmware\n```\n\n\n\nArchitecture and repository URL\n===============================\n\nTo bootstrap the system, you will need to find the correct URL of the Ubuntu\nrepository to fetch packages from, and the architecture of the system you are\ngoing to install.\n\nWe export this into the `REPO_URL` and `TARGET_ARCH` environment variables.\n\nRemember, Raspberry Pis, AWS Graviton instances, M1/M2 Macs are ARM64!\n\n### Ubuntu repositories\n\nNormally you will want to use a local mirror of the Ubuntu repositories. In\nmy case (Germany), I'll use the `de` mirror.\n\nFor `ARM64`, the repository base URL will be:\n\n```shell\nexport REPO_URL=\"http://de.ports.ubuntu.com/ubuntu-ports\"\nexport TARGET_ARCH=\"arm64\"\n```\n\nFor `X86_64` the repository base URL will be:\n\n```shell\nexport REPO_URL=\"http://de.archive.ubuntu.com/ubuntu\"\nexport TARGET_ARCH=\"amd64\"\n```\n\n### AWS repositories\n\nCanonical provides mirrors of the various Ubuntu repositories in each AWS\nregion, so you can use those for speed. You can get the region calling:\n\n```shell\nexport AWS_REGION=\"$(curl --silent http://169.254.169.254/latest/meta-data/placement/region)\"\n```\n\nThen for `ARM64` the repository base URL will be:\n\n```shell\nexport REPO_URL=\"http://${AWS_REGION}.clouds.ports.ubuntu.com/ubuntu-ports\"\nexport TARGET_ARCH=\"arm64\"\n```\n\nWhile for `X86_64` the repository base URL will be:\n\n```shell\nexport REPO_URL=\"http://${AWS_REGION}.clouds.archive.ubuntu.com/ubuntu\"\nexport TARGET_ARCH=\"amd64\"\n```\n\n\n\nBootstrapping the system\n========================\n\nNow we can simply use `debootstrap` to install the basics of the OS:\n\n```shell\ndebootstrap --arch=\"${TARGET_ARCH}\" --variant=minbase --include=systemd,curl,ca-certificates noble /mnt \"${REPO_URL}\"\n```\n\nOnce `debootstrap` is finished, we can mount the various filesystems required by\nthe installation:\n\n```shell\nmount -t proc proc \"/mnt/proc\"\nmount -t sysfs sysfs \"/mnt/sys\"\nmount -o bind /run \"/mnt/run\"\nmount -o bind /dev \"/mnt/dev\"\nmount -o bind /dev/pts \"/mnt/dev/pts\"\n```\n\nAnd then `chroot` into our new environment using the `C` locale (as no other\nlocale has yet been generated):\n\n```shell\neval $(LANG=C LC_ALL=C LANGUAGE=C locale) chroot \"/mnt\" /bin/bash --login\nbind 'set enable-bracketed-paste off'\n```\n\n\n\nOperating system installation\n=============================\n\nWe continue the installation by configuring some basic files.\n\nFirst of all, we need to prepare the `/etc/hostname` and `/etc/hosts` files:\n\n```shell\n# Set the generic \"ubuntu\" host name\necho \"ubuntu\" \u003e \"/etc/hostname\"\n\n# Basic \"hosts\" file for \"ubuntu\"\ncat \u003e \"/etc/hosts\" \u003c\u003c EOF\n127.0.0.1 localhost\n127.0.1.1 ubuntu ubuntu.local\nEOF\n```\n\nThen we'll set up the `/etc/mtab` link and `/etc/fstab` file:\n\n```shell\nln -sf \"/proc/self/mounts\" \"/etc/mtab\"\n\ncat \u003e \"/etc/fstab\" \u003c\u003c EOF\n$(printf \"# %-41s %-15s %-5s %-17s %-5s %s\" \"PARTITION\" \"MOUNTPOINT\" \"TYPE\" \"OPTIONS\" \"DUMP\" \"FSCK\")\n$(printf \"UUID=%-38s %-15s %-5s %-17s %-5s %s\" $(findmnt -no UUID,TARGET,FSTYPE \"${ROOT_DEV}\") \"defaults,discard\" \"0\" \"1\")\n$(printf \"UUID=%-38s %-15s %-5s %-17s %-5s %s\" $(findmnt -no UUID,TARGET,FSTYPE \"${BOOT_DEV}\") \"umask=0077\" \"0\" \"1\")\nEOF\n```\n\nWe then need to prepare our sources list for APT in `/etc/apt/sources.list`:\n\n```shell\ncat \u003e \"/etc/apt/sources.list\" \u003c\u003c EOF\ndeb ${REPO_URL} noble main restricted universe multiverse\ndeb ${REPO_URL} noble-updates main restricted universe multiverse\ndeb ${REPO_URL} noble-security main restricted universe multiverse\nEOF\n```\n\nThen we want to configure an extra source for our _minimal os packages_:\n\n```shell\ncurl -sSLo \"/usr/share/keyrings/minimal-ubuntu.gpg\" \\\n  \"https://usrz.github.io/minimal-ubuntu/minimal-ubuntu.gpg\"\n\ncat \u003e \"/etc/apt/sources.list.d/minimal-ubuntu.list\" \u003c\u003c EOF\ndeb [signed-by=/usr/share/keyrings/minimal-ubuntu.gpg] https://usrz.github.io/minimal-ubuntu nodistro main\nEOF\n```\n\nWe then want to update the system, and install all packages required for a\nminimal system.\n\nThe `minimal-os` package provided here is a meta-package that requires only\na minimal set of dependencies, and provides some basic system configuration.\n\n```shell\nexport DEBIAN_FRONTEND=noninteractive\n\nexport APT_OPTIONS=\"\\\n  -oAPT::Install-Recommends=false \\\n  -oAPT::Install-Suggests=false \\\n  -oAcquire::Languages=none\"\n\napt-get $APT_OPTIONS update \u0026\u0026 \\\n  apt-get $APT_OPTIONS --yes dist-upgrade \u0026\u0026 \\\n  apt-get $APT_OPTIONS --yes install minimal-os\n```\n\nThen for sanity's sake, let's keep only the `minimal-os` package marked as\n_manually installed_ (this will help with `apt-get autoremove`):\n\n```shell\napt-mark showmanual | xargs apt-mark auto\napt-mark manual minimal-os\n```\n\n\nKernel and helper packages\n==========================\n\nNext step is to install a kernel and our helper package for AWS EC2 or Raspberry\nPi systems.\n\n### AWS EC2 kernel\n\nFor AWS EC2 instances, the `minimal-ec2-os` package will install `grub` and its\nconfigurations on the root device. The kernel comes from `linux-aws`:\n\n```shell\napt-get --yes install linux-aws minimal-ec2-os\n```\n\nAfter installing, as AWS EC2 boot via UEFI and `grub` we need to configure the\n`grub` bootloader with some sensible defaults for EC2:\n\n```shell\n# Basic minimal GRUB configuration supporting EC2 serial console\ncat \u003e \"/etc/default/grub\" \u003c\u003c EOF\nGRUB_DEFAULT=0\nGRUB_TIMEOUT=5\nGRUB_TIMEOUT_STYLE=\"menu\"\nGRUB_CMDLINE_LINUX_DEFAULT=\"nomodeset console=tty1 console=ttyS0\"\nGRUB_DISTRIBUTOR=\"Minimal Ubuntu OS\"\nGRUB_DISABLE_RECOVERY=\"true\"\nEOF\n\n# Install GRUB on the boot device, and update the kernel\ngrub-install \"${BASE_DEV}\"\nupdate-grub\n```\n\n### Raspberry Pi kernel\n\nFor the Raspberry Pi, we don't need a boot loader, and the `minimal-rpi-os` will\ntake care of preparing the `/boot/firmware` filesystem for booting:\n\n```shell\napt-get --yes install linux-raspi minimal-rpi-os\n```\n\n### Other systems\n\nFor all other systems the `linux-generic` kernel is a good starting point:\n\n```shell\napt-get --yes install linux-generic grub-efi\n```\n\nWe then need to configure the `grub` boot loader, as a starting point use:\n\n```shell\n# Basic minimal GRUB configuration\ncat \u003e \"/etc/default/grub\" \u003c\u003c EOF\nGRUB_DEFAULT=0\nGRUB_TIMEOUT=0\nGRUB_TIMEOUT_STYLE=\"hidden\"\nGRUB_CMDLINE_LINUX_DEFAULT=\"nomodeset console=tty1\"\nGRUB_DISTRIBUTOR=\"Minimal Ubuntu OS\"\nEOF\n\n# Install GRUB on the boot device, and update the kernel\ngrub-install \"${BASE_DEV}\"\nupdate-grub\n```\n\n\n\nBonus Packages\n==============\n\n### NodeJS 22.x\n\n```shell\ncurl -sSL \"https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key\" \\\n  | gpg1 --dearmor \u003e \"/usr/share/keyrings/nodesource.gpg\"\n\ncat \u003e \"/etc/apt/sources.list.d/nodesource.list\" \u003c\u003c EOF\ndeb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main\nEOF\n\napt-get update\napt-get install nodejs\n```\n\n### Tailscale\n\n```shell\ncurl -sSLo \"/usr/share/keyrings/tailscale.gpg\" \\\n  \"https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg\"\n\ncat \u003e \"/etc/apt/sources.list.d/tailscale.list\" \u003c\u003c EOF\ndeb [signed-by=/usr/share/keyrings/tailscale.gpg] https://pkgs.tailscale.com/stable/ubuntu noble main\nEOF\n\napt-get update\napt-get install tailscale\n```\n\n### Cloudflare\n\n```shell\ncurl -sSLo \"/usr/share/keyrings/cloudflared.gpg\" \\\n  \"https://pkg.cloudflare.com/cloudflare-main.gpg\"\n\ncat \u003e \"/etc/apt/sources.list.d/cloudflared.list\" \u003c\u003c EOF\ndeb [signed-by=/usr/share/keyrings/cloudflared.gpg] https://pkg.cloudflare.com/cloudflared noble main\nEOF\n\napt-get update\napt-get install cloudflared\n```\n\n\n\nUser login\n==========\n\nThe `minimal-os` package installs the `ubuntu` user. If you need to log in interactively (e.g. from the console) set the password now:\n\n```shell\npasswd ubuntu\n```\n\nIf you need SSH access, we only allow SSH keys. Place the authorized SSH public\nkey in `/home/ubuntu/.ssh/authorized_keys`.\n\n\u003e **NOTE**: If you are in EC2-land, upon first boot the SSH key will be\n\u003e automatically downloaded from AWS, and configured to whatever key you have\n\u003e setup when creating the instance.\n\nFinally, we allow **password-less sudo** for the `ubuntu` user. If this is not\nto your liking, take a peek at the `/etc/sudoers.d/00-minimal-os` file.\n\n\n\nCleaning up\n===========\n\nLet's clean up our APT state and exit the chrooted environment:\n\n```shell\napt-get clean \u0026\u0026 exit\n```\n\nWe then want to clean up a bunch of files left over by the installation, some\nof them will be re-created once `minimal-ec2-os-setup` runs the first time:\n\n```shell\nrm -f /mnt/etc/ssh/ssh_host_*_key* \\\n      /mnt/root/.bash_history \\\n      /mnt/var/log/alternatives.log \\\n      /mnt/var/log/apt/* \\\n      /mnt/var/log/bootstrap.log \\\n      /mnt/var/log/dpkg.log\n```\n\nAt this point we can simply unmount our volume:\n\n```shell\numount -Rlf /mnt\n```\n\nIf needed (for example in case of an EC2 volume to create an AMI, or a disk\nimage for a Raspberry Pi) we can clean out any unused block in our root\nfilesystem:\n\n```shell\nzerofree -v \"${ROOT_DEV}\"\n```\n\n\n### Creating an AMI\n\nFirst of all clean out any unused block in our root filesystem:\n\nGoing back to the EC2 console we can now detach the volume we created from the\nEC2 instance we used for setup, and create a snapshot from it.\n\nOnce the snapshot is created, we can then create an image from it (remember,\nselect the snapshot then _Actions -\u003e Create image from snapshot_, it is **not**\nin the _AMI_ section of the console).\n\nRemember to select the following:\n* _Architecture_: either `arm64` or `x86_64`\n* _Root device name_: always `/dev/sda1`\n* _Virtualization type_: always `Hardware-assisted virtualization`\n* _Boot mode_: always `UEFI`\n\n\n### Raspberry Pi image\n\nRemeber to detach the loopback device of our image, first:\n\n```shell\nlosetup -d ${BASE_DEV}\n```\n\nThen you can use `dd` to write the image on an SD card or USB drive.\n\nRemember, if you're doing this process on a Mac, using `/dev/rdiskX` is a lot\nfaster than using `/dev/diskX`, so, depending on what device your SD card or\nUSB drive maps to, use something like this:\n\n```shell\nsudo dd if=\"raspberry-os.img\" of=\"/dev/rdisk10\" bs=4M conv=fsync status=progress\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fusrz%2Fminimal-ubuntu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fusrz%2Fminimal-ubuntu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fusrz%2Fminimal-ubuntu/lists"}