Base System Setup

This step installs a very basic Arch Linux system to be used in later steps to build out the home environment. Depending on how you plan on doing things, you may need more than one of these - my personal setup runs on one firewall and one container host. I assume most readers will start with a Windows environment - I trust that if you already run Linux or a BSD as your desktop, you know to perform basic tasks like creating boot media. This is all described on the Arch Wiki (https://wiki.archlinux.org/title/installation_guide). but since my setup is a little opinionated, the instructions I use are here.

1) Create boot media

I prefer Rufus (https://rufus.ie/en/) as my image writer under Windows. Download the Arch Linux installation image from https://archlinux.org/download/#checksums and perform whatever verification steps you feel needed to verify this image is what it purports to be. Then open Rufus, select the downloaded image and your USB drive, and click Start.

2) Initial bootup

Once the system has booted, make it remotely accessible - it's much easier to follow a guide when you can copy and paste than having to type everything out on a separate keyboard or a VM's console window. I am assuming your current setup has a DHCP server for the internal network - any commercial home router will suffice.

# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:15:5d:ff:fd:24 brd ff:ff:ff:ff:ff:ff
inet 172.16.32.15/16 metric 100 brd 172.16.255.255 scope global dynamic eth0
valid_lft 525207sec preferred_lft 525207sec
inet6 fe80::215:5dff:feff:fd24/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever

Note the block starting with "eth0:". Your network interface is likely to be named differently - this output was generated on a Hyper-V virtual machine - but you want to see the host's IP address.

# passwd
# systemctl start sshd
# ssh-keygen -l -f /etc/ssh/etc/ssh/ssh_host_ed25519_key.pub

At this point, you should be able to point an SSH client (PuTTY, https://www.putty.org/ is my preference in a Windows environment) to connect to the IP address you identified earlier and log in as root with the password you just set. The last line of the preceding instruction block outputs the server's key fingerprint for ED25519. Compare to what PuTTY reports the fingerprint to be to be certain you are connecting to the correct server.

3) Preparing the file systems

Identify the disk to install the system on.

# ls -la /dev/disk/by-id
total 0
drwxr-xr-x 2 root root 100 Jun  1 15:28 .
drwxr-xr-x 9 root root 180 Jun  1 15:28 ..
lrwxrwxrwx 1 root root   9 Jun  1 15:28 scsi-14d534654202020207305e3437703544694957d7ced624a7d -> ../../sr0
lrwxrwxrwx 1 root root   9 Jun  1 15:28 scsi-3600224805ecebbae2edb7b0020a459d9 -> ../../sda
lrwxrwxrwx 1 root root   9 Jun  1 15:28 wwn-0x600224805ecebbae2edb7b0020a459d9 -> ../../sda

This out put, is again, from a Hyper-V virtual machine. For comparison, on my own home lab, this looks like

total 0
drwxr-xr-x 2 root root 380 Jun  2 12:16 .
drwxr-xr-x 9 root root 180 Jun  2 12:16 ..
lrwxrwxrwx 1 root root   9 Jun  2 12:16 ata-JMicron_H_W_RAID5_HMB72YSD22COCPCDMGUJ -> ../../sda
lrwxrwxrwx 1 root root  10 Jun  2 12:16 ata-JMicron_H_W_RAID5_HMB72YSD22COCPCDMGUJ-part1 -> ../../sda1
lrwxrwxrwx 1 root root  13 Jun  2 12:16 nvme-eui.00000000000000000026b7381d5bf295 -> ../../nvme0n1
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-eui.00000000000000000026b7381d5bf295-part1 -> ../../nvme0n1p1
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-eui.00000000000000000026b7381d5bf295-part2 -> ../../nvme0n1p2
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-eui.00000000000000000026b7381d5bf295-part3 -> ../../nvme0n1p3
lrwxrwxrwx 1 root root  13 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29 -> ../../nvme0n1
lrwxrwxrwx 1 root root  13 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29_1 -> ../../nvme0n1
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29_1-part1 -> ../../nvme0n1p1
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29_1-part2 -> ../../nvme0n1p2
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29_1-part3 -> ../../nvme0n1p3
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29-part1 -> ../../nvme0n1p1
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29-part2 -> ../../nvme0n1p2
lrwxrwxrwx 1 root root  15 Jun  2 12:16 nvme-KINGSTON_OM8SEP4512N-A0_50026B7381D5BF29-part3 -> ../../nvme0n1p3
lrwxrwxrwx 1 root root   9 Jun  2 12:16 usb-External_USB3.0_DISK00_20170331000DA-0:0 -> ../../sda
lrwxrwxrwx 1 root root  10 Jun  2 12:16 usb-External_USB3.0_DISK00_20170331000DA-0:0-part1 -> ../../sda1

If your home lab is a half-way modern mini-PC, your target disk will likely have a line starting with nvme-eui. On a VM, it's likely to be scsi or wwn. Note that on VMWare ESXi in particular, manual configuration is needed to make the virtual disks report a WWN. This might require extra surgery, beyond the scope of this blog.

# gdisk /dev/disk/by-id/wwn-0x600224805ecebbae2edb7b0020a459d9
GPT fdisk (gdisk) version 1.0.10

Partition table scan:
MBR: not present
BSD: not present
APM: not present
GPT: not present

Creating new GPT entries in memory.

Command (? for help):

# n
Command (? for help): n
Partition number (1-128, default 1):
First sector (34-167772126, default = 2048) or {+-}size{KMGTP}:
Last sector (2048-167772126, default = 167770111) or {+-}size{KMGTP}: +512M
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): ef00
Changed type of partition to 'EFI system partition'

# n
Partition number (2-128, default 2):
First sector (34-167772126, default = 1050624) or {+-}size{KMGTP}:
Last sector (1050624-167772126, default = 167770111) or {+-}size{KMGTP}: +32G
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): 8200
Changed type of partition to 'Linux swap'

Partition number (3-128, default 3):
First sector (34-167772126, default = 68159488) or {+-}size{KMGTP}:
Last sector (68159488-167772126, default = 167770111) or {+-}size{KMGTP}:
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300):
Changed type of partition to 'Linux filesystem'

This sets up your initial partitions, but you're not done in gdisk. Note that for the swap partition (code 0x8200), I generally tend to allocate the amount of physical RAM my host has. Back to your gdisk session:

# p
Disk /dev/disk/by-id/wwn-0x600224805ecebbae2edb7b0020a459d9: 167772160 sectors, 80.0 GiB
Sector size (logical/physical): 512/4096 bytes
Disk identifier (GUID): 78206F52-B486-4B6D-889E-01B0F1F751DE
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 167772126
Partitions will be aligned on 2048-sector boundaries
Total free space is 4029 sectors (2.0 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
1            2048         1050623   512.0 MiB   EF00  EFI system partition
2         1050624        68159487   32.0 GiB    8200  Linux swap
3        68159488       167770111   47.5 GiB    8300  Linux filesystem

# c
Partition number (1-3): 1
Enter name: EFI

# c
Partition number (1-3): 2
Enter name: swap

# c
Partition number (1-3): 3
Enter name: root

# p
Disk /dev/disk/by-id/wwn-0x600224805ecebbae2edb7b0020a459d9: 167772160 sectors, 80.0 GiB
Sector size (logical/physical): 512/4096 bytes
Disk identifier (GUID): AE1F0C3C-4AB3-4897-9974-1333B3DBB35D
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 167772126
Partitions will be aligned on 2048-sector boundaries
Total free space is 4029 sectors (2.0 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
1            2048         1050623   512.0 MiB   EF00  EFI
2         1050624        68159487   32.0 GiB    8200  swap
3        68159488       167770111   47.5 GiB    8300  root

# w
Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!

Do you want to proceed? (Y/N): y
OK; writing new GUID partition table (GPT) to /dev/disk/by-id/wwn-0x600224805ecebbae2edb7b0020a459d9.
The operation has completed successfully.

Now, verify the partition table was written and you can see your new partitions.

# ls -la /dev/disk/by-partlabel
total 0
drwxr-xr-x  2 root root 100 Jun  3 21:10 .
drwxr-xr-x 11 root root 220 Jun  3 21:10 ..
lrwxrwxrwx  1 root root  10 Jun  3 21:10 EFI -> ../../sda1
lrwxrwxrwx  1 root root  10 Jun  3 21:10 root -> ../../sda3
lrwxrwxrwx  1 root root  10 Jun  3 21:10 swap -> ../../sda2

So far, so good. Next is creating our actual file systems.

# mkswap -L swap /dev/disk/by-partlabel/swap
Setting up swapspace version 1, size = 32 GiB (34359734272 bytes)
LABEL=swap, UUID=47346730-1a7d-46af-b55e-6156de83a783

# mkfs -t xfs -L root /dev/disk/by-partlabel/root
meta-data=/dev/disk/by-partlabel/root isize=512    agcount=4, agsize=3112832 blks
=                       sectsz=4096  attr=2, projid32bit=1
=                       crc=1        finobt=1, sparse=1, rmapbt=1
=                       reflink=1    bigtime=1 inobtcount=1 nrext64=1
data     =                       bsize=4096   blocks=12451328, imaxpct=25
=                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1
log      =internal log           bsize=4096   blocks=16384, version=2
=                       sectsz=4096  sunit=1 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
Discarding blocks...Done.

# mkfs -t msdos -F32 -n EFI /dev/disk/by-partlabel/EFI
mkfs.fat 4.2 (2021-01-31)

# ls -la /dev/disk/by-label
total 0
drwxr-xr-x  2 root root 120 Jun  3 21:20 .
drwxr-xr-x 11 root root 220 Jun  3 21:10 ..
lrwxrwxrwx  1 root root   9 Jun  1 15:28 ARCH_202405 -> ../../sr0
lrwxrwxrwx  1 root root  10 Jun  3 21:20 EFI -> ../../sda1
lrwxrwxrwx  1 root root  10 Jun  3 21:17 root -> ../../sda3
lrwxrwxrwx  1 root root  10 Jun  3 21:15 swap -> ../../sda2

4) Mount file systems and install basic system packages

# mount /dev/disk/by-label/root /mnt
# mkdir /mnt/boot
# mount /dev/disk/by-label/EFI /mnt/boot
# swapon /dev/disk/by-label/swap

These instructions make the root filesystem available at /mnt, create a mountpoint for the EFI system partition and /mnt/boot, and mount it there. Finally, the swap space is made available to the system.

# pacstrap -K /mnt base

There'll be lots of output from this. As long as you don't get a line like

==> ERROR: Failed to install packages to new root

Everything should be in order. Next, generate an fstab on the new install.

# genfstab -L /mnt | tee /mnt/etc/fstab
# /dev/sda3 UUID=9185823b-2a91-42db-a0d6-2b61ee131e8d
LABEL=root              /               xfs             rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota  01

# /dev/sda1 UUID=3837-6595
LABEL=EFI               /boot           vfat            rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro      0 2

# /dev/sda2 UUID=47346730-1a7d-46af-b55e-6156de83a783
LABEL=swap              none            swap            defaults        0 0

Since I'm sometimes going to have to edit those files, I prefer to have labels over UUIDs. At this point, the system is ready to be finished up and made bootable on its own.

5) Finishing touches

Execute a chroot to the newly installed system. From here on, all commands will be run restricted to the new system and not affect the installation environment.

# arch-chroot /mnt

Notice the changed look of the prompt. We're still missing quite a few things for a working system, even if we're only planning to use this to build things from. So, let's finish up our package installs.

# pacman -Syu rust git pkgconf sudo debugedit fakeroot

`

Confirm the package selection. We'll get rid of those before we're done, but we need them for now.

# git clone https://github.com/Morganamilo/paru.git
# cd paru
# git checkout v2.0.3
# cargo build --release
# cp target/release/paru /usr/local/bin

This installs the package manager paru. It's a front-end for Arch Linux' pacman that is also capable of automatically building and installing AUR packages. It can also be run as a non-root user and performs sudo operations if needed, prompting for a password. I prefer it over stock pacman. We'll reinstall and properly configure paru to clean up after itself later, but for now, this will do.

# paru -Syu linux-lts linux-firmware vim less openssh

Select to install dracut as initramfs provider, and confirm the installation. Vim is my preferred text editor, less is a pager, openssh will provide remote access to the system.

# ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime
# hwclock --systohc --utc

I generally prefer to keep system-wide time on servers in UTC. Individual users can set TZ to their preferred time zone. Then set the hardware clock to system time.

# echo 'C.UTF-8 UTF-8' > /etc/locale.gen

Since this is a server, no other locales should be needed, although if you prefer localized console messages, a command like

# echo 'de_DE.UTF-8 UTF-8' >> /etc/locale.gen

will configure other locales, in this example, German. Note the double '>' in the second command, it instructs the system to append, rather than overwrite.

# locale-gen
# echo 'LANG=C.UTF-8' > /etc/locale.conf

Generate the locale and set the system-wide language to C.UTF-8. Individual users can override this by setting LANG in their environment.

I personally don't need to configure vconsole.conf, but if you have a non-US keyboard, you may need something like

# echo 'KEYMAP=de-latin1' > /etc/vconsole.conf

to properly set up your keyboard. This example sets a German 105-key QWERTZ keymap.

# echo 'homelab-base' > /etc/hostname

sets the hostname to 'homelab-base'. It will do for now, until we get to specializing our hosts.

# dracut --uefi --kver 6.6.32-1-lts --zstd --host-only --quiet

Generates the boot image. We'll automate that later on, but this time around, we need to do it manually.

# systemctl enable sshd
# passwd

Set the root password and enable the SSH server to run after reboot.

# bootctl install
# systemctl enable systemd-boot-update

Installs the bootloader. The complaint about the random seed file being world accessible can be ignored - we'll fix that in a bit. The system is, at this point, basically bootable, but we still need to set up networking and configure a user to log in with - openssh disables root logins with a password by default.

# useradd -G wheel -m <username>
# passwd <username>

Create user and set the password. You probably should consider setting up SSH public key authentication, but that is beyond this blog entry's scope. I personally use a YubiKey for key storage.

# systemctl enable systemd-networkd
# systemctl enable systemd-resolved

Edit the file /etc/systemd/network/20-wired.network to contain

[Match]
Name=<your interface name - see section 2>

[Network]
DHCP=yes

I am assuming here that your host will not be running wireless and there's a DHCP server on the network.

Enable the non-root user to sudo: Edit /etc/sudoers.conf, find the line read

# %wheel ALL=(ALL:ALL) ALL

and remove the leading hash mark and space. Now reboot the system you just installed and ssh back into it.

Next, set up the resolver configuration:

# ln -sf /run/systemd/resolve/stub-resolve.conf /etc/resolve.conf

The last steps are to reinstall paru from the AUR, rather than in /usr/local, clean up all the residue from having compiled an application, and configure paru to clean up behind itself. Starting with paru:

$ paru -Syu paru
$ sudo rm /usr/local/bin/paru

Edit /etc/paru.conf, and uncomment the options "RemoveMake", "SudoLoop", "CombinedUpgrade" and "NewsonUpgrade".

$ paru -S dracut-ukify

Edit /etc/dracut-ukify.conf and append the line "ukify_global_args+=(--cmdline "root=LABEL=root")".

$ sudo dracut-ukify -a

Kernel updates now automatically generate the UKI to boot from.

$ sudo sed -i -e 's/0022/0077/g' /etc/fstab

This resolve the earlier warning from bootctl install.

$ paru -R rust
$ paru -Qdttq | paru -Rns -

Clean up build-time dependencies from earlier.

$ sudo systemctl reboot

Finally, reboot again to verify everything is in order. This installs a base Arch Linux system to use for the next blog posts.

Subscribe to Homelab Adventures

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe