Bypassing the Installer: Manually Installing Ubuntu
Introduction
Sometimes you need a bit more flexibility when installing Linux (but not so much that you want to install Arch). For my personal stuff, I tend to use BTRFS on-top of LUKS, and just BTRFS on KVM guests. Both configurations aren't exactly widely supported by installers, but no matter, this is Linux...
So this is how I manually install Ubuntu/Debian when I need to. This can also be considered a good foundation for making custom Debian images.
For installations, I use the official installers, but switch to a console after booting (basically treating it like a live OS). I've also used rescue images, depending on the server provider (looking at you Hetzner...).
Installing
Pre-Checks
First I check for UEFI support on the machine - virtual or otherwise. I do that by checking if the UEFI was discovered by the kernel during boot:
If a file exists, then UEFI is supported and should be used. This will change how the bootloader is installed.
Also, make sure all the needed tooling is installed:
And yes, it's been pointed out to me the irony of using Arch install helpers to install Ubuntu the Arch way.
Prepare the Disk(s)
First, it's best to start clean - the following finds and removes the partition table on /dev/sda
.
Then moving on to partitioning, I use parted
due to it's modern support of > 2TiB
disks.
Of course, you can create three partitions instead to support both UEFI and BIOS boot. Since I don't typically move installs from machine to machine, I don't worry about introducing that complexity (read, I want to forget BIOS boot when I can, UEFI is a safe-space).
UEFI Boot
If running on a machine that supports UEFI, I use the following:
The first partition will be mounted to /boot/efi
and will contain the first-stage bootloader.
BIOS Boot
On machines that don't support UEFI (older machines or KVM virtual machines, by default), the command changes:
Technically the first partition only needs a few MiB for it's first-stage bootloader, but storage is cheap. This partition will not have a filesystem, it's only used to reserve a little space for Grub (MBR boot is kind of crazy).
Prepare BTRFS Sub-Volumes
This is completely optional, but I like to create separate volumes when using BTRFS, for two reasons:
- Since BTRFS uses Copy-on-write (COW) - snapshots are a first-class citizen (read, awesome). Moving the root filesystem to a sub-volume makes booting from a snapshot easier/cleaner.
- Also since BTRFS uses COW, swap files are not a first-class citizen (read, special rules need to be followed).
The
@
name for the root filesystem sub-volume is a common naming-convention, but it can really be named anything. Some automatic snapshotting programs need this to be@
.
Mount the Root Filesystem
Now that this BTRFS volume has sub-volumes, everything can be mounted in the "final" configuration.
I use
noatime
by default for BTRFS volumes (disables last file access times). This is another side-effect of using a COW filesystem - metadata modifications are heavier compared to more traditional journaling filesystems (e.g. EXT4, NTFS, etc.). The COW filesystem needs to "copy" the metadata "on write" instead of directly modifying the data.
Install the Base System
At this point, we can start installing stuff. Debian makes this easy with the debootstrap
script:
There's a couple of options here.
--arch=amd64
hopefully obvious.--variant=minbase
this is a preset that installs "essential packages and apt". The system won't boot, but at least we can chroot in later.jammy
is the suite for the mirrorhttp://archive.ubuntu.com/ubuntu/
, which is wheredebootstrap
will pull packages from to create the base system. These should be whatever distribution is being installed.
Note that this may take a while. Has anyone noticed that mirrors are slow on Tuesdays - does everyone patch on Tuesdays?
To make things easier, I generate a basic fstab
of the current system layout.
This will be modified later as genfstab
, I just like my chroot to be as "authentic" as possible.
Chroot
After installing the base system, it's time to chroot!
To prepare for the chroot, we need to mount the host's kernel interfaces into the future chroot. I also mount /tmp
to tmpfs
to keep the disk clean (for creating images).
And finally we can chroot
without stuff breaking:
Install the Meta-Packages
At this point I can run standard commands to complete the installation in this chroot
.
First I update the list of mirrors, debootstrap
only adds one source:
Of course, use your local mirrors.
Then I use the the standard meta-packages to install a more complete OS.
It's kind of hard to figure out what's in each of these meta-packages, this is my summary:
ubuntu-standard
- rather standard stuff (hehe). Packages likecron
,man-db
,lsof
, andrsync
. It's a no-brainer to install.ubuntu-minimal
- also super minimal. Packages likesudo
,less
, andmount
. Most of these packages are installed by--variant=minbase
, but it doesn't hurt to add the meta-package.ubuntu-server
- now this one can be skipped if needed. It contains packages that make operator lives easier. Things likegnupg
,htop
,tmux
, andbtrfs-progs
. I install it because I'm learning to love myself.
There are other meta-packages - although, they aren't exactly documented succinctly. For server installations, I think these three meta-packages are a good foundation. If building a desktop, the desktop metapackages should be used.
Generate Locales
You might notice that apt
had some warnings about missing locales. They can be add/enabled with the following (of course, substituting to your locale):
If you didn't get any warnings, it's safe to run the commands anyway - making sure your locales are good, helps avoid issues with misbehaving terminals.
Set Time Zone
To set the time zone without SystemD's timedatectl
working (since this is a chroot), a symlink can be used.
In this example, my time zone is America/Chicago
. A list can be found here.
Setup Swap
Again, since I use BTRFS, I make my swap on the sub-volume created. This is all documented on BTRFS's docs.
Install the Kernel
This one is easy, install the kernel you want. I use HWE kernels, so I install linux-generic-hwe-*
.
There's a couple options here (assuming Ubuntu):
linux-generic
the standard kernel, best for bare-metal installs.linux-virtual
a kernel optimized for virtual installs (may not contain things like USB support).
And if installing in a public cloud, there's a few kernels optimized for those hypervisors, for example:
linux-ibm
linux-azure
linux-aws
The
linux-azure
kernel can also be used for Hyper-V virtual machines.
Install Grub
Then I customize Grub defaults. This is optional, Grub's defaults are sane.
A couple of things to note:
noplymouth
disables the splash screen during boot. The kernel will fallback to outputting logs to the terminal. I don't get why vendors like adding splash screens...console=ttyS0,115200 console=tty0
and friends, enables the serial console - useful for KVM VM's and for other headless deployments.GRUB_RECORDFAIL_TIMEOUT=5
is used as Grub doesn't quite handle BTRFS correctly when detecting if the previous boot attempt was clean. This oddities causesGRUB_TIMEOUT=5
to be ignored.
Then to finalize Grub's install (to actually install the bootloader), grub-install
and update-grub
can be used.
UEFI Boot
--no-nvram
can be used prevent adding a new boot entry to your system (stored by the motherboard).--bootloader-id
is the name of the NVRAM entry - the name doesn't matter, it's for us, humans.
BIOS Boot
Note that when installing Grub for BIOS boot, the command should target the disk, not a partition. Grub will write the first-stage bootloader before the first partition (and may overflow into the dummy partition that was created above).
If you target a partition, you might get a weird error about not having enough space. Ask me how I know...
Customize the Install
Remember to configure networking if this is all being done on a remote server...
At this point, a bootable install should exist, but I like to customize a few more things. This is also a good time to install your configuration manager, if you have one, e.g. SaltStack, etc.
Since this is Ubuntu, networking is configured with Netplan, for example to configure ens18
:
I know Netplan gets some hate, but it's been a joy to use, at least personally. I'm also a software engineer first, so YAML is second-nature. I like schema validation when writing configuration files!
It also might be a good idea to set a root
password, depending on how this install is going to be used. If this is an image, it's best for the configuration manager or cloud-init to set these.
Or better, add some SSH keys:
Finalize the Install
Almost done, only a few more things to look at. Let's exit chroot:
And it's time to update fstab
to include the swap file:
I also run a few "find-and-replace" commands to fix some of the verbosity of genfstab
.
And I like to check that everything looks correct:
And it does, so rebooting time! (or saving and sysprep'ing for imaging)
And done! A nice clean, and fully custom installation.
Postfix
Preparing for Imaging
When building QEMU images for deployment, SysPrep should be used to at least:
- Force the server to generate SSH server keys on next boot.
- Reset any machine ids (which controls "first boot" detection, e.g. if using cloud-init).
I use the following:
And to make the image smaller:
Sources
- Install Debian with Debootstrap + Grub EFI · GitHub
- chroot - ArchWiki
- Lot's of man pages.