Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save PhillCli/254560b915e16e9f7e8a8e236307bd12 to your computer and use it in GitHub Desktop.

Select an option

Save PhillCli/254560b915e16e9f7e8a8e236307bd12 to your computer and use it in GitHub Desktop.
Arch Linux installation (BTRFS+LUKS2+Limine)

Arch Linux installation notes

LUKS2 encrypted BTRFS system partition with Limine/Snapper integration and hybernate to swapfile

Table of Contents


Introduction

This is my typical installation of Arch Linux with LUKS2 encrypted BTRFS system partition on an UEFI system. This document does not replace the official Arch Linux installation guide and the accompanying documentation. It is merely a condensed note of the most important steps, intended for my personal use. Use it at your own risk, and keep in mind that some of the instructions below may be outdated or unsuitable for your specific case.

The following features are characteristic of this installation:

  • The system partition is encrypted with LUKS2 and formatted with the BTRFS file system using subvolumes.
  • The ESP partition (FAT32) is not encrypted and is mounted as /boot (if that's not secure enough for someone, additional measures can be taken, such as using Secure Boot).
  • I like small, fast and sexy lightweight bootloaders, which is why Limine is the choice of mine. It has a wonderful integration with Snapper also.
  • The installation also provides functional network connectivity after the reboot.
  • The goal of the installation is a system intended to be further configured for everyday desktop use.

Preparation

Download the official Arch Linux ISO.

Check which USB stick you are going to install an ISO to with lsblk.

Format the USB with dd.

sudo dd bs=4M if=archlinux-<your-date>-x86_64.iso of=/dev/<your-usb> status=progress oflag=sync

Ensure proper network connectivity

The assumption is that a laptop is being used, intended to connect to the Internet via a wireless connection.

iwctl station <device> connect <SSID>

To continue setup of your machine via SSH, check if the sshd service is running and set the root password via passwd.

systemctl status sshd.service
passwd

Check you IP address with ip addr show. SSH from another machine via ssh root@<ip>.

Preconfigure shell

Use the command localectl list-keymaps or something like ls /usr/share/kbd/keymaps/**/*.map.gz | grep bg to list keymaps. Or just set:

loadkeys pl
setfont ter-v24b

Optionally turn off the pcspeaker as well: setterm -blength 0.

Confirm the system is UEFI

I usually explicitly use UEFI 64-bit systems such as the Lenovo ThinkPad X1, T470s, T480s, T14, T14s, and other similar models, but you can check whether the current system is one by running the command:

cat /sys/firmware/efi/fw_platform_size

If this command prints 64 or 32 then the current system is UEFI.

Disk partitioning

N.B. The assumption here is that we are setting up a new computer that will run only Arch Linux. No dual boot with Windows or other Linux installations.

One partition is needed for the ESP, which will be used for /boot with a FAT32 file system. The remaining disk space will be encrypted with LUKS2 and formatted with BTRFS. This setup allows for flexible management of subvolumes within the container for specific directories, including snapshots or swap.

I prefer to have enough space in /boot for various purposes (including possibly multiple kernel versions), so I usually make this partition at least 2GB.

To start fresh, let's wipe all existing partitions on the disk. If there's any chance you might miss something currently stored on this disk, now is the time to proactively make a backup.

sgdisk --zap-all /dev/nvme0n1

Tools like fdisk, cfdisk, cgdisk, and others can be used here, but parted has the advantage of being script-friendly.

parted --script /dev/nvme0n1 \
    mklabel gpt \
    mkpart ESP fat32 1MiB 10240MiB \
    set 1 esp on \
    mkpart Linux btrfs 10240MiB 100%

Format the ESP partition

Most probably the partition will be /dev/nvme0n1p1 (partition 1 on disk nvme0n1), but this can be checked with lsblk command.

Let's format the ESP partition as FAT32.

mkfs.fat -F 32 /dev/nvme0n1p1

Prepare the system partition

Create LUKS encrypted container on the system partition

Simple as that:

cryptsetup luksFormat /dev/nvme0n1p2

It should be more than obvious that the password used to encrypt this system partition must not be random and obvious, but it also must not be forgotten.

Later, the UUID of this container will be needed, which can be obtained using the command cryptsetup luksUUID /dev/nvme0n1p2 or the command ls -l --time-style=+ /dev/disk/by-uuid/. Make sure to save this UUID somewhere.

Create the BTRFS layout inside the LUKS container

First, open the container (should appear as /dev/mapper/root using the command below):

cryptsetup open /dev/nvme0n1p2 root

Format it as BTRFS:

mkfs.btrfs /dev/mapper/root

Mount (temporarily) the newly created btrfs file system in \mnt:

mount /dev/mapper/root /mnt

And let's make some subvolumes.

btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@var_log
btrfs subvolume create /mnt/@var_cache
btrfs subvolume create /mnt/@snapshots

At a later stage, another one for swap (and hibernation) will be added. However, this is done from the comfort of an already installed and functioning system. It is not critical to perform this manually at this point.

Let's unmount the temporarily mounted file system and mount the subvolumes instead (incl. the the vfat ESP partition). If you don't want compression just remove the compress option, but level 1 is great for NMVE disks. Also noatime is fine for personal desktop setup.

umount /mnt
mount -o compress=zstd:1,noatime,subvol=@ /dev/mapper/root /mnt
mount --mkdir -o compress=zstd:1,noatime,subvol=@home /dev/mapper/root /mnt/home
mount --mkdir -o compress=zstd:1,noatime,subvol=@var_log /dev/mapper/root /mnt/var/log
mount --mkdir -o compress=zstd:1,noatime,subvol=@var_cache /dev/mapper/root /mnt/var/cache
mount --mkdir -o compress=zstd:1,noatime,subvol=@snapshots /dev/mapper/root /mnt/.snapshots
mount --mkdir /dev/nvme0n1p1 /mnt/boot

And we are ready to go with the installation itself.

Installing the Arch Linux

Let's install some packages. Many guides will tell you that pacstrap -K /mnt base linux linux-firmware is more than enough. Even if that’s true, there are somehow higher expectations...

pacman -Syy
pacstrap -K /mnt base base-devel linux linux-zen linux-lts linux-firmware git vim neovim btrfs-progs efibootmgr limine cryptsetup iwd impala reflector zsh bash-completion acpi acpi_call acpid alsa-utils pipewire pipewire-alsa pipewire-pulse pipewire-jack wireplumber firewalld bluez bluez-utils cups util-linux terminus-font openssh man man-pages sudo rsync amd-ucode

N.B. Replace amd-ucode with intel-ucode if you have an Intel processor.

Make fstab file from your current filesystem layout. Edit it manually in case of some obvious errors.

genfstab -U /mnt >> /mnt/etc/fstab

It's time to chroot to our /mnt point.

arch-chroot /mnt

Set the local time properly. Use your time zone.

ln -sf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime
hwclock --systohc

In the file /etc/locale.gen uncomment all the needed locales. In my case en_US.UTF-8 UTF-8 and pl_PL.UTF-8 UTF-8. Save the changes in the file and execute command locale-gen after that. At the end add the English locale in /etc/locale.conf.

vim /etc/locale.gen
locale-gen
echo LANG=en_US.UTF-8 > /etc/locale.conf

Create /etc/vconsole.conf file and add the following inside it. (FONT variable is optional)

vim /etc/vconsole.conf
KEYMAP=pl
FONT=ter-v24b

Set a hostname.

echo arch > /etc/hostname

Set up the root password

passwd

Add a new user, add it to wheel group and set a password.

useradd -mG wheel mgajewskik
passwd mgajewskik

Execute the following command and uncomment the line to let members of group wheel execute any program

EDITOR=vim visudo

Modify /etc/mkinitcpio.conf to have btrfs in MODULES, /usr/bin/btrfs in BINARIES, and sd-encrypt in HOOKS. Add sd-encrypt hook after block and before filesystems.

If hibernation is to be used, resume needs to be added (somewhere after udev). If it is from a swap file inside an encrypted container (as in this case), then resume should be placed after the sd-encrypt and filesystem hooks.

MODULES=(btrfs)
...
BINARIES=(/usr/bin/btrfs)
...
HOOKS=(base systemd autodetect microcode modconf kms keyboard keymap sd-vconsole block sd-encrypt filesystems fsck)
...

Don't forget to execute mkinitcpio -P command.

Setting up Limine bootloader

Limine setup on UEFI systems is very simple. Just make /boot/EFI/limine directory and copy the relevant file BOOT file there. For more detailed instructions check the Limine page in the Arch wiki.

mkdir -p /boot/EFI/limine
cp /usr/share/limine/BOOTX64.EFI /boot/EFI/limine/

Now we need to create an entry for Limine in the NVRAM:

efibootmgr --create --disk /dev/nvme0n1 --part 1 \
      --label "Arch Linux Limine Bootloader" \
      --loader '\EFI\limine\BOOTX64.EFI' \
      --unicode

N.B. --disk /dev/nvme0n1 --part 1 means /dev/nvme0n1p1

N.B. Do NOT include boot when pointing to Limine Bootloader file.

N.B. In a Limine config, boot():/ represents the partition on which limine.conf is located.

Finally let's make a basic configuration for Limine. Use the command vim /boot/EFI/limine/limine.conf and write inside the following (or at least the first of the two sections):

timeout: 3

/Arch Linux
    protocol: linux
    path: boot():/vmlinuz-linux
    cmdline: quiet rd.luks.name=<device-UUID>=root root=/dev/mapper/root rw rootflags=subvol=@ rootfstype=btrfs
    module_path: boot():/initramfs-linux.img

/Arch Linux (fallback)
    protocol: linux
    path: boot():/vmlinuz-linux
    cmdline: quiet rd.luks.name=<device-UUID>=root root=/dev/mapper/root rw rootflags=subvol=@ rootfstype=btrfs
    module_path: boot():/initramfs-linux-fallback.img

Remember being advised to save an UUID? Now is the time to use it. Replace the <device-UUID> above with the UUID of your LUKS container.

Final steps

Networking

Enable systemd-networkd, systemd-resolved and iwd services before rebooting. Otherwise, you won't be able to connect.

systemctl enable iwd
systemctl enable systemd-networkd
systemctl enable systemd-resolved
systemctl enable bluetooth
systemctl enable cups
systemctl enable firewalld
systemctl enable reflector.timer

Add configuration files for systemd-networkd setup, check network interface with ip link show:

/etc/systemd/network/25-wireless.network

[Match]
Name=wlp2s0

[Link]
RequiredForOnline=routable

[Network]
DHCP=yes
IgnoreCarrierLoss=3s

or link default examples from the documentation: https://wiki.archlinux.org/title/Systemd-networkd#Configuration

ln -s /usr/lib/systemd/network/80-wifi-station.network.example /etc/systemd/network/80-wifi-station.network
ln -s /usr/lib/systemd/network/89-ethernet.network.example /etc/systemd/network/89-ethernet.network

Disable the PC speaker

I don't like the sound of the PC speaker so I like to blacklist kernel modules responsible for it. To do so, let's create the /etc/modprobe.d/nobeep.conf file with contents:

blacklist pcspkr
blacklist snd_pcsp

https://wiki.archlinux.org/title/PC_speaker#Disabling_the_PC_speaker

Setup lid behaviour

To change the lid behaviour modify the /etc/systemd/logind.conf file:

HandlePowerKey=ignore
HandlePowerKeyLongPress=poweroff
HandleLidSwitch=suspend
HandleLidSwitchExternalPower=suspend
HandleLidSwitchDocked=ignore

Enable color output in pacman

Uncomment Color from /etc/pacman.conf

Exit from chroot and reboot

It's time to exit the chroot, unmount the /mnt, close the crypted container and reboot to the newly installed Arch Linux.

exit
umount -R /mnt
cryptsetup close root
reboot

Do not forget to unplug the installation media (USB stick).

Enjoy!

Useful post-install steps

Act as root, otherwise use sudo.

Configure networking

A working network configuration needs to be adjusted according to this guide. In my case I just need to do (once again):

iwctl station <device> connect <SSID>

For convenience, an impala TUI can be used to make this wireless connection.

To make the systemd-resolved work properly in a stub mode, link the default configuration to the /etc/resolv.conf and restart the service:

ln -sf ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl restart systemd-resolved.service

https://wiki.archlinux.org/title/Systemd-resolved#DNS

SSH access

If you want to continue using SSH, make sure the sshd service is enabled and edit the /etc/ssh/sshd_config to include the following lines. Remember to revert this change back! You don't want to leave open SSH root access to your machine.

PermitRootLogin yes
PasswordAuthentication yes

Then restart the sshd service with:

systemctl enable --now sshd.service

Additional packages

A set of useful stuff...

pacman -Syu wget htop gvfs gvfs-smb imagemagick usbutils bat zip unzip brightnessctl noto-fonts ttf-jetbrains-mono libreoffice-fresh libreoffice-fresh-pl firefox ghostty ttf-nerd-fonts-symbols-common ttf-nerd-fonts-symbols ttf-jetbrains-mono-nerd fzf fd ripgrep starship yazi eza go tmux udiskie zsh-completions zsh-autosuggestions bind pavucontrol inetutils nmap

Optional (only for Intel video):

pacman -Syu intel-media-driver mesa vulkan-intel

Install yay

It is not recommended to use makepkg as root, so this step is supposed to be performed with a user account.

sudo pacman -S --needed git base-devel && git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -si

Time

Check if ntp is active and if the time is right. Enable and start the time synchronization service (usually it is not enabled).

timedatectl
timedatectl set-ntp true

TRIM support

Check if the SSD drive supports TRIM with the command lsblk --discard. Non-zero values of DISC-GRAN (discard granularity) and DISC-MAX (discard max bytes) indicate TRIM support.

The util-linux package provides fstrim.service and fstrim.timer systemd unit files. Enabling the timer will activate the service weekly. This is the so-called periodic TRIM.

pacman -S --needed util-linux
systemctl enable --now fstrim.timer

Pacman hook for Limine

Add /etc/pacman.d/hooks/99-limine.hook file with the following content:

[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = limine              

[Action]
Description = Deploying Limine after upgrade...
When = PostTransaction
Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/limine/

Swap and hibernation

Create swapfile in swap subvolume and enable it. I use swap to hibernate my laptop so, the size is the size of my RAM.

btrfs subvolume create /swap
btrfs filesystem mkswapfile --size 35g --uuid clear /swap/swapfile
swapon -p 0 /swap/swapfile

Add the following in the /etc/fstab:

/swap/swapfile none swap defaults,pri=0 0 0

Add resume in /etc/mkinitcpio.conf HOOKS...

HOOKS=(base systemd autodetect microcode modconf kms keyboard keymap sd-vconsole block sd-encrypt filesystems resume fsck)

... and execute:

mkinitcpio -P

Note: It is not mandatory anymore (on UEFI systems) but when using the resume= kernel parameter, it must point to the unlocked/mapped device (/dev/mapper/root) that contains the file system with the swap file i.e. findmnt -no UUID -T /swap/swapfile.

To find the proper swap offset run:

sudo btrfs inspect-internal map-swapfile -r /swap/swapfile

Check all possible commands (some will not be available before reboot): https://wiki.archlinux.org/title/Power_management/Suspend_and_hibernate#High_level_interface_(systemd)

Snapper

yay -Syu snapper limine-snapper-sync limine-mkinitcpio-hook

Add sd-btrfs-overlayfs at the end of the HOOKS in /etc/mkinitcpio.conf and execute mkinitcpio -P

The /.snapshots directory needs to be unmounted and removed before proceeding. Snapper will create it on it's own during the setup process.

umount /.snapshots
rm -rf /.snapshots
snapper -c root create-config /
snapper -c home create-config /home
sudo mount -a
sudo sed -i 's/^TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/{root,home}
sudo sed -i 's/^NUMBER_LIMIT="50"/NUMBER_LIMIT="10"/' /etc/snapper/configs/{root,home}
sudo sed -i 's/^NUMBER_LIMIT_IMPORTANT="10"/NUMBER_LIMIT_IMPORTANT="10"/' /etc/snapper/configs/{root,home}

Copy configuration from /etc/limine-snapper-sync.conf to /etc/default/limine if it not already present. And change in /etc/default/limine:

MAX_SNAPSHOT_ENTRIES=10
LIMIT_USAGE_PERCENT=85
ROOT_SNAPSHOTS_PATH="/@snapshots"

In /etc/limine-snapper-sync.conf and /etc/default/limine find and disable/remove (if not used):

#COMMANDS_BEFORE_SAVE="limine-reset-enroll" 
#COMMANDS_AFTER_SAVE="limine-enroll-config"

Create (if it isn't there yet) /boot/limine.conf file and add //Snapshots inside. Replace <machine-id> with the output from the command cat /etc/machine-id.

term_font_scale: 2x2

/+Arch Linux
comment: Arch Linux
comment: machine-id=<machine-id>
  
    //Linux
    protocol: linux
    path: boot():/vmlinuz-linux
    cmdline: quiet cryptdevice=UUID=XXXXXX-XXXXXX:root root=/dev/mapper/root rw rootflags=subvol=@ rootfstype=btrfs resume=UUID=YYYYY-YYYYYY resume_offset=ZZZZZZZ
    module_path: boot():/initramfs-linux.img

    //Snapshots

There will be an additional Limine config file in the /boot/EFI/limine/limine.conf that will take precedence during the boot process. Remove it or back it up somewhere as we want to use the new /boot/limine.conf file that will be synced with Snapper.

Execute the command limine-snapper-sync and if there are no errors systemctl enable --now limine-snapper-sync.service.

Open the /boot/limine.conf file to check if the boot order has not changed which can happen if many kernels were added along the standard linux kernel. Change the ordering if that happened.

Available (important) commands are:

  • limine-snapper-list - displays the current Limine snapshot entries
  • limine-snapper-sync - synchronizes Limine snapshot entries with the Snapper list
  • limine-snapper-info - provides information about versions, the total number of bootable snapshots, etc.
  • limine-snapper-restore - restores the system, including matching kernel versions, from a selected bootable snapshot

Snapshot creation

To create a snapshot of the root subvolume run:

sudo snapper -c root create -c number -d "first manual snapshot"

The snapshot should be automatically added to the /boot/limine.conf file and should be available during the boot process.

Optionally install snap-pac. It triggers snapper to create snapshots during system updates.

pacman -Syu snap-pac

To create a backup of the home subvolume run: sudo snapper -c home create -c number -d "description"

Snapshot restore

To restore a root snapshot, reboot and select which snapshot to boot from. After the reboot run sudo limine-snapper-restore and reboot again.

To restore desired files from the home snapshot run: sudo snapper -c home undochange <old_number>..0 /path/to/file/or/dir

More information regarding Snapper integration with Limine (on Btrfs) here: https://wiki.archlinux.org/title/Limine#Snapper_snapshot_integration_for_Btrfs

Automatic firmware updates

pacman -Syu fwupd udisks2
fwupdmgr get-devices
fwupdmgr refresh
fwupdmgr get-updates
fwupdmgr update
systemctl enable --now fwupd-refresh.timer

Install graphical environment

For the graphical environment I will use:

  • UWSM for setting up Wayland compositors
  • MangoWC as my main Wayland compositor
  • Noctalia as a minimal desktop shell
yay -S uwsm libnewt mangowc fuzzel noctalia-shell xdg-desktop-portal xdg-desktop-portal-wlr xdg-desktop-portal-gnome xdg-desktop-portal-gtk wl-clipboard wl-clipboard-x11 wl-clip-persist slurp hyprshot satty hypridle vicinae cliphist nemo nemo-audio-tag nemo-image-converter nemo-fileroller papirus-icon-theme phinger-cursors arc-gtk-theme nwg-look

Add this to your .zprofile or .profile file to kickstart uwsm on each login shell:

if uwsm check may-start && uwsm select; then
  exec uwsm start default
fi

I will be using ZSH, so I am creating the .zprofile file and running: chsh -s $(which zsh)

I will also install Hyprland as a spare compositor:

yay -S hyprland hyprland-protocols nwg-displays xdg-desktop-portal-hyprland

Now run systemctl reboot and enter your newly configured system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment