Arch Linux install crib sheet

Writing these notes helped me to overcome my hesitation about deploying a Linux distribution with such a ‘manual’ installation procedure. And along the way I discovered that it really wasn’t all that complicated ; there are just a great many forks in the road. I hope these notes may be of some assistance to others.

Table of contents:

Build specifications

This is a fairly simple build, with these goals:

The network configuration is DHCP / IPv6 autoconfig (SLAAC) to start, but there is a section that discusses static addressing.

(Ordinarily I wouldn’t configure systemd-resolved, but in this instance, I’m using it to handle the DHCP-assigned DNS servers.)


First, disable Secure Boot, as the Arch installion image doesn’t support it. Then, boot the Arch ISO, and execute the following commands. Note that the fdisk commands are somewhat abbreviated ; you’ll figure it out.

# ip addr				# check that DHCP did its thing
# ls /sys/firmware/efi/efivars		# check that it booted in EFI mode

# fdisk /dev/sda	# or use lsblk or fdisk -l to find it
g			# create a new GPT partition table
n 1 2048 +1G		# create the EFI System Partition (ESP)
t 1			# change partition 1 to "EFI System"
n 2 [defaults]		# create the LVM partition
t 2 43			# change partition 2 to "Linux LVM"

# pvcreate /dev/sda2				# create the LVM physical volume
# vgcreate filament /dev/sda2			# create the LVM volume group
# lvcreate -n swap_0 -l 256 filament		# create the swap volume (256 extents = 1 GB)
# lvcreate -n root -l 100%FREE filament		# create the root volume

# mkfs.fat -F32 /dev/sda1		# format the ESP
# mkfs.ext4 /dev/filament/root		# format the root filesystem
# mkswap /dev/filament/swap_0		# create the swap
# swapon /dev/filament/swap_0		# activate swap

# mount /dev/filament/root /mnt		# mount /
# mkdir /mnt/boot			# make a space for the boot partition
# mount /dev/sda1 /mnt/boot		# mount /boot

# alias vi=vim				# fix annoyances :-)
# vi /etc/pacman.d/mirrorlist		# leave only the desired mirror(s)
# pacman -Sy archlinux-keyring		# prevent PGP signature failures
# pacstrap /mnt base linux linux-firmware lvm2 efibootmgr vim	# add packages

# genfstab -U /mnt >> /mnt/etc/fstab	# create /etc/fstab in the target

# # A bit out of order but it has to be done outside of the chroot:
# ln -sf /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf

# arch-chroot /mnt			# chroot to the target system

At this point, you’re in a chroot of the new system. Things you might want to change for your own setup are in italics.

[root@archiso /]# printf '\nalias vi=vim\n' >> /etc/bash.bashrc
[root@archiso /]# source /etc/bash.bashrc	# fix missing vi permanently

[root@archiso /]# ln -sf /usr/share/zoneinfo/America/Vancouver /etc/localtime
[root@archiso /]# hwclock --systohc		# creates /etc/adjtime

[root@archiso /]# vi /etc/locale.gen		# choose your locale(s)
[root@archiso /]# locale-gen			# generate locales
[root@archiso /]# echo "LANG=en_CA.UTF-8" > /etc/locale.conf	# set locale

[root@archiso /]# echo filament > /etc/hostname		# set hostname

[root@archiso /]# vi /etc/mkinitcpio.conf	# add lvm2 to HOOKS (before filesystems)
[root@archiso /]# mkinitcpio -P			# make /boot/initramfs-linux.img
[root@archiso /]# efibootmgr --verbose --disk /dev/sda --part 1 --create \
	--label "Arch Linux" --loader /vmlinuz-linux --unicode \
	"root=UUID=`lsblk -no uuid /dev/filament/root` rw initrd=\\initramfs-linux.img"

[root@archiso /]# systemctl enable systemd-networkd
[root@archiso /]# systemctl enable systemd-resolved
[root@archiso /]# cat >/etc/systemd/network/ <<EOF
[root@archiso /]# mkdir /etc/systemd/resolved.conf.d
[root@archiso /]# printf '[Resolve]\nLLMNR=no\n' > \
   /etc/systemd/resolved.conf.d/20-disable-llmnr.conf		# disable LLMNR

[root@archiso /]# passwd root
[root@archiso /]# exit

Now the system is ready. Unmount disks:

# umount /mnt/boot
# umount /mnt

You can power off and take a snapshot, clone the disk, etc ; or reboot and proceed directly to using Arch.

Optional extras

There are a few things you may want to add to the above. You can install anything you like during the installation chroot, but don’t start it.

Add a user

# useradd -m username
# passwd username

nftables (firewall)

The stock configuration is a good start, but I had some trouble with the rate limiting bits sending RST instead of silently dropping packets to blocked ports. So I comment out those lines.

# pacman -S nftables
# sed -i 's,\( *\)\(.*counter\),\1#\2,' nftables.conf		# comment out 'counter' lines
# systemctl enable nftables
# systemctl start nftables	# but not in the installation environment

open-vm-tools (vmtoolsd)

# pacman -S open-vm-tools
# systemctl enable vmtoolsd
# systemctl start vmtoolsd	# but not in the installation environment

openssh (sshd)

# pacman -S openssh
# systemctl enable sshd
# systemctl start sshd		# but not in the installation environment

Static IP addresses

My favourite setup is to set a static IPv4 address and add  a static IPv6 address to the autoconfigured one, with privacy extensions enabled. This way outbound traffic uses autoconfigured IPv6 addresses, and inbound traffic can be received on the static one.

Static IPv4 address

This is very straightforward. Use multiple Address= lines if you need more than one address on this interface. If you have more than one interface, add additional .network files as needed.

# cat > /etc/systemd/network/ << EOF

Because IPv6 will be autoconfigured anyway – unless you take steps to disable it, which I don’t recommend – I keep the same IPv6 options as in the DHCP scenario (IPv6PrivacyExtensions=yes).

I recommend you don’t bother setting a DNS= line in the network definition, and take this opportunity to get rid of systemd-resolved.

# systemctl disable systemd-resolved
# systemctl stop systemd-resolved
# rm -r /etc/systemd/resolved.conf.d
# rm /etc/resolv.conf			# this was the 'stub' one from systemd-resolved
# echo nameserver > /etc/resolv.conf

Finally, restart systemd-networkd to reconfigure the adapter(s), and use ip addr to check the result.

# systemctl restart systemd-networkd
# ip addr

IPv6 autoconfig (default) + static IPv6 address

This configuration is useful if you want to run a server (eg, httpd) listening on a static IPv6 address, but you also want outbound connections to use a dynamic, temporary address.

Just add an Address= line (or multiple lines) to the file. No need for Gateway= as the RA will hand out the gateway address already.

# echo Address=2001:470:eb96:2::ffff/64 >> /etc/systemd/network/

You could also optionally add an IPv6 DNS server:

# echo nameserver 2001:470:eb96:2::1 >> /etc/resolv.conf

Finally, restart systemd-networkd to reconfigure the adapter(s), and use ip addr to check the result.

# systemctl restart systemd-networkd
# ip addr

IPv6 autoconfig + static IPv6 address (default)

This could be handy if you want to leave autoconfig enabled, but you want your default IPv6 source address to be static. [Many thanks to Celada on Server Fault  for the answer to this vexing question.]

Translating that into a format systemd-networkd likes looks like this:

# cat > /etc/systemd/network/ << EOF
# systemctl restart systemd-networkd
# ip addr

Essentially, this marks IPv6 addresses (eg autoconfig addresses) as label 99 (a “martian label”), except  the preferred source address, which gets label 1. Thus the static address is chosen as the source address.

Apparently the outbound source will be the static address for all destinations except the local subnet. Let’s test that:

# (tcpdump -c 2 -npti ens192 host 2001:470:eb96:2::1 2>/dev/null &) ; sleep 1 ; \
	ping -c 1 2001:470:eb96:2::1 >/dev/null ; sleep 1
IP6 2001:470:eb96:2:e97c:9a9c:5ffd:ba52 > 2001:470:eb96:2::1: ICMP6, echo request, id 28, seq 1, length 64
IP6 2001:470:eb96:2::1 > 2001:470:eb96:2:e97c:9a9c:5ffd:ba52: ICMP6, echo reply, id 28, seq 1, length 64
# (tcpdump -c 2 -npti ens192 host 2607:f8b0:400a:801::2003 2>/dev/null &) ; sleep 1 ; \
	ping -c 1 2607:f8b0:400a:801::2003 >/dev/null ; sleep 1
IP6 2001:470:eb96:2::ffff > 2607:f8b0:400a:801::2003: ICMP6, echo request, id 29, seq 1, length 64
IP6 2607:f8b0:400a:801::2003 > 2001:470:eb96:2::ffff: ICMP6, echo reply, id 29, seq 1, length 64

Yes, that’s exactly what happens.

Static IPv6 address (no autoconfig)

If you really need to use a static IPv6 address for all outbound connections, this is the section for you.

It took quite a bit of trial and error to get the magic incantation to prevent IPv6 autoconfiguration (SLAAC) while not completely breaking routing ; there were a lot of false starts with IPv6AcceptRA=no (don’t do that).

[Why not IPv6AcceptRA=no? Routing doesn’t seem to work properly with this set, even if you also set  Gateway=. You can ‘force’ it to work by pinging the address from another system, causing NDP to fire on the IPv6 router, but this is hardly reliable.]

The essential bit is PrefixDenyList=. That should be set to the prefix(es) that your router normally hands out. Or, you can use PrefixAllowList= with a bogus network if you want to allow none.

# cat > /etc/systemd/network/ << EOF
# systemctl restart systemd-networkd
# ip addr

I can’t say that I really recommend this, though perhaps there are some edge cases for it.

Cloning considerations

In these examples, templatevm is the name of the template VM, and newvm is the clone.

Cloning virtual disks in ESXi

Rather than actually cloning a VM, typically I just build a new VM without a hard drive, then copy the VMDK from a template VM. The NVRAM has to be copied, too, as the Arch Linux EFI bootloader entry is there.

# cd /vmfs/volumes/datastore1/templatevm
# vmkfstools -i templatevm.vmdk ../newvm/newvm.vmdk
Destination disk format: VMFS zeroedthick
Cloning disk 'templatevm.vmdk'...
Clone: 100% done.
# cp templatevm.nvram ../newvm/newvm.nvram
cp: overwrite '../newvm/newvm.nvram'? y

Then I attach the new VMDK to the new VM and boot it.

Personalizing cloned systems

Most of the installation is already very generic, except:

  1. The hostname in /etc/hostname.
  2. The LVM volume group name.
  3. LVM and filesystem UUIDs.
  4. SSH server keys (if you started sshd in your template system).

Changing the hostname

Changing the hostname requires no more than adjusting /etc/hostname.

# echo newvm > /etc/hostname

Changing the LVM volume group name

This, too, is very straightforward:

# vgrename templatevm newvm

Strictly speaking, this isn’t necessary, as /etc/fstab uses filesystem UUIDs to mount the volumes ; though you may find it convenient to have the volume group name match the hostname.

Regenerating SSH server keys

If you started sshd in the template VM, it will generate SSH server keys, which shouldn’t be the same for any two hosts. To fix that, you need to remove the keys on the cloned system and regenerate them.

Caution: be very careful you don’t introduce any extra spaces near the * in this command!

# rm /etc/ssh/ssh_host_*_key*
# systemctl restart sshdgenkeys

Attaching cloned LVM volumes to another system

Normally, it’s not a problem for many systems to share the same LVM UUIDs or volume group names. There is one scenario, though, where it will conflict: when you attach LVM volumes to another system that was cloned from the same template. Here’s how to deal with that situation. This example assumes that you have attached a second disk to a VM that had the same ‘parent’.

# pvdisplay
  WARNING: Not using device /dev/sdb2 for PV H18lfO-g5sN-hZSH-LPV3-yBbB-TdFL-2fQbQE.
  WARNING: PV H18lfO-g5sN-hZSH-LPV3-yBbB-TdFL-2fQbQE prefers device /dev/sda2 because device is used by LV.
  --- Physical volume ---
  PV Name		/dev/sda2
  VG Name		darkstar
  PV Size		<15.00 GiB / not usable 2.00 MiB
  Allocatable		yes (but full)
  PE Size		4.00 MiB
  Total PE		3839
  Free PE		0
  Allocated PE		3839
  PV UUID		H18lfO-g5sN-hZSH-LPV3-yBbB-TdFL-2fQbQE

Although there are two PVs attached (sda2 and sdb2), only one shows up because of the UUID conflict. Use vgimportclone to change both PV and VG UUIDs of the second disk.

# vgimportclone /dev/sdb2
# pvdisplay -C
  PV         VG        Fmt  Attr PSize   PFree
  /dev/sda2  darkstar  lvm2 a--  <15.00g    0
  /dev/sdb2  darkstar1 lvm2 a--  <15.00g    0
# vgchange -a y darkstar1
  2 logical volume(s) in volume group "darkstar1" now active

The PV and VG UUIDs are different now, so both VGs can be used independently. Notice that the second VG has been renamed (again to avoid conflict) ; but if you don’t like the name it gave, you can always use vgrename.

Interestingly, the LV and filesystem (ext4/swap) UUIDs have not been changed. There is no way – and apparently no reason – to change the LV UUID. But you can change the filesystem (ext4/swap) UUIDs. This would avoid any potential conflict with EFI booting and /etc/fstab entries that used a UUID.

Caution: If you do this, the original system (ie, the one that this second drive belonged to) will no longer be bootable, unless you modify both its EFI bootloader entry and /etc/fstab.

# e2fsck -f /dev/darkstar1/root
e2fsck 1.46.5 (30-Dec-2021)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/darkstar1/root: 41645/917504 files (0.1% non-contiguous), 485673/3668992 blocks
# tune2fs -U random /dev/darkstar1/root
tune2fs 1.46.5 (30-Dec-2021)
Setting the UUID on this filesystem could take some time.
Proceed anyway (or wait 5 seconds to proceed) ? (y,N) y

For swap, it’s even easier, just mkswap and a new UUID will be generated:

# mkswap /dev/darkstar1/swap_0
mkswap: /dev/darkstar1/swap_0: warning: wiping old swap signature.
Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)
no label, UUID=24cb7b64-e287-4552-8a0c-f257197b2973


  1. ArchWiki

Appendix: Why Arch Linux?

Until recently, I was a (relatively) happy Ubuntu user. Although I had a number of small issues with it, I worked through them, until I encountered this:

$ sudo apt install vim
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  alsa-topology-conf alsa-ucm-conf libasound2 libasound2-data libcanberra0 libltdl7 libogg0 libpython3.8 libtdb1 libvorbis0a
  libvorbisfile3 sound-theme-freedesktop
Suggested packages:
  libasound2-plugins alsa-utils libcanberra-gtk0 libcanberra-pulse ctags vim-doc vim-scripts
The following NEW packages will be installed:
  alsa-topology-conf alsa-ucm-conf libasound2 libasound2-data libcanberra0 libltdl7 libogg0 libpython3.8 libtdb1 libvorbis0a
  libvorbisfile3 sound-theme-freedesktop vim
0 upgraded, 13 newly installed, 0 to remove and 0 not upgraded.
Need to get 3,884 kB of archives.
After this operation, 12.1 MB of additional disk space will be used.
Do you want to continue? [Y/n]

I don’t want to install sound libraries on a headless server just to run vim! And neither Ubuntu nor Debian (upstream) seem interested in fixing this. (“It’s a feature, not a bug!”)

Arch, on the other hand, did a totally reasonable thing, by compiling with sound support in gvim, but not ‘plain’ vim.

So, this was the proverbial straw that broke the camel’s back. I guess I should thank the Debian package maintainers for botching their vim build, and the Ubuntu package maintainers for not fixing it in their release either, so that I was pushed to explore other Linux distributions.

Contact me with any questions, comments, or feedback: