Installing Alpine Linux with full disk encryption on BIOS/MBR systems with a custom partition layout

This document describes how to install Alpine Linux on BIOS/MBR systems with a fully encrypted hard drive (including /boot) and a custom partition layout. If you don't need a custom partition layout because Alpine's default (a swap partition and a single partition for everything else) suits you, you can simply use the setup-alpine script, which is way more convenient than what is described below.

This guide works for x86_64 machines and should work for 32-bit x86 machines (i686 and later) just as well.

The method chosen here is “LVM on LUKS”, basically meaning that we will be creating an encrypted container on the hard drive, set up logical volumes inside of it, and then install Alpine onto those logical volumes.

Note: While this document already provides a working solution, it is still a work in progress.

Warning: It is assumed that you're using a hard disk drive (magnetic platters spinning under a read/write head). The disk wiping operation shown below is not meant for SSDs because it will put unnecessary wear on such devices and likely not get rid of all existing data in the process.


1. Downloading and verifying the installation image

Alpine installation images are available at This guide uses the extended image because it already contains most of the software needed to install Alpine. This has the advantage that the package manager and networking can be set up later in the installation process, which is especially handy if the process needs to be interrupted at some point.

In addition to the image itself, also download the accompanying checksum file (*.sha256) and GPG signature (*.asc) into the same directory. Then use these files to verify the integrity and authenticity of the downloaded image.

1.1 Comparing the checksum

On Linux and FreeBSD systems, verifying the checksum usually works as follows:

$ sha256sum -c alpine-extended-3.18.0-x86_64.iso.sha256 
alpine-extended-3.18.0-x86_64.iso: OK

On NetBSD:

$ cksum -a sha256 -c alpine-extended-3.18.0-x86_64.iso.sha256 && echo OK

On OpenBSD:

$ sha256 -C alpine-extended-3.18.0-x86_64.iso.sha256 alpine-extended-3.18.0-x86_64.iso 
(SHA256) alpine-extended-3.18.0-x86_64.iso: OK

[TODO: Explain how to do this on MacOS and Windows.]

1.2 Verifying the signature

To verify the GPG signature, do the following:

$ gpg --verify alpine-extended-3.18.0-x86_64.iso.asc

For a proper signature, the output should look mostly like this:

gpg: assuming signed data in 'alpine-extended-3.18.0-x86_64.iso'
gpg: Signature made Tue May  9 21:51:41 2023 CEST
gpg:                using RSA key 0482D84022F52DF1C4E7CD43293ACD0907D9495A
gpg: Good signature from "Natanael Copa <>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 0482 D840 22F5 2DF1 C4E7  CD43 293A CD09 07D9 495A

If Alpine's public key is not already on your keyring, GPG won't be able to verify the signature:

$ gpg --verify alpine-extended-3.18.0-x86_64.iso.asc 
gpg: assuming signed data in 'alpine-extended-3.18.0-x86_64.iso'
gpg: Signature made Tue May  9 21:51:41 2023 CEST
gpg:                using RSA key 0482D84022F52DF1C4E7CD43293ACD0907D9495A
gpg: Can't check signature: No public key

To obtain the key, run gpg --recv-keys with the key ID in GPG's output:

$ gpg --recv-keys 0482D84022F52DF1C4E7CD43293ACD0907D9495A
gpg: key 293ACD0907D9495A: public key "Natanael Copa <>" imported
gpg: Total number processed: 1
gpg:               imported: 1

Then repeat the verification command.

2. Creating a bootable installation medium

In order to create a bootable installation medium, the image needs to be applied to some sort of storage device, e.g., a USB key or a DVD.

2.1 Applying the image to a USB key

Warning: The following operation will destroy all data currently residing on the USB key you use for it!

Applying the image to a USB key can be done using the cat command on the image and redirecting its output to the device file representing the USB key, for example:

$ cat alpine-extended-3.18.0-x86_64.iso > /dev/sdX

Be sure to replace the name of the image with the name of the image you downloaded and double-check you have the correct device name.

2.2 Applying the image to a DVD

On Linux and BSD systems, you can use cdrskin to write the installation image to a DVD via the command line.

For example, to burn the image onto a DVD at a speed of 4x and with verbose command output, run

cdrskin -v speed=4 image.iso

replacing image.iso with the file name of the image you want to burn to the DVD.

If there is only one optical disc drive on your system, it should be auto-detected. If there is more than one, find the device name using cdrskin --devices and then specify it with the dev parameter. For example, if your optical drive of choice is /dev/sr0, run

cdrskin -v dev=/dev/sr0 speed=4 image.iso

3. Preparing the disk

Warning: The following process will destroy all data currently residing on the hard drive you use for it!

Note: As this part of the installation does not need network access, you can keep the machine disconnected form the network for now.

Boot from the installation medium and log in as root. No password is required.

If you don't want to use the standard US keymap, run setup-keymap before doing anything else.

3.1 Installing the required software

Install the required software:

# apk add cryptsetup e2fsprogs grub grub-bios lvm2 mkinitfs util-linux

If you prefer to use Btrfs instead of Ext4, replace e2fsprogs with btrfs-progs.

If you're not comfortable using the vi editor, you can also add Nano:

# apk add nano

3.2 Wiping

To ensure that

the whole disk must be filled with random data before setting up encryption. (Cf. Wiping disk vs. initializing container.)

First, take a look at available disks, using the lsblk command and locate the one you want to install Alpine on. On the machine used for writing this guide, the output looks like this:

# lsblk
loop0    7:0    0 157.4M  1 loop /.modloop
sda      8:0    0 298.1G  0 disk
└─sda1   8:1    0 298.1G  0 part
sdb      8:16   1  14.5G  0 disk /media/sdb
├─sdb1   8:17   1   840M  0 part
└─sdb2   8:18   1   1.4M  0 part
sr0     11:0    1  1024M  0 rom

Here, /dev/sda represents the internal hard drive, /dev/sdb the installation medium. Thus, Alpine is going to be installed to /dev/sda.

Wiping the disk merely requires redirecting the content of /dev/urandom to the respective device file:

Warning: This operation will irrecoverably destroy all data on the target device! Double-checking for the correct device name is strongly advised.

# cat /dev/urandom > /dev/sdX

This operation will take a while, depending on your hardware. The machine I used to do the test installations for this guide took about one hour to fill its 300-gigabyte hard drive.

Note: If you need to, you can shut the computer down for now and continue later. You would only have to install the packages installed in 3.1 again (and maybe set your keymap before that, as in 3.) after booting from the installation medium next time.

3.3 Pre-partitioning

Use fdisk to create a new DOS/MBR partition table and one partition spanning the whole disk:

# fdisk /dev/sda
Welcome to fdisk (util-linux 2.38.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): o
Created a new DOS disklabel with disk identifier 0xb68308e1.

Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1): 
First sector (2048-625142447, default 2048): 
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-625142447, default 

Created a new partition 1 of type 'Linux' and of size 298.1 GiB.

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.

Scan for new device nodes:

# mdev -s

3.4 Creating the crypt container

Now set up the new partition as the encrypted device, using cryptsetup. Basically, like this:

# cryptsetup --verbose --verify-passphrase luksFormat --type luks1 /dev/sda1

We're using LUKS1 here because GRUB didn't support LUKS2 at the time of writing.

Here's a security-optimized variant of the above command:

# cryptsetup --verbose --verify-passphrase --cipher serpent-xts-plain64 --key-size 512 --hash sha512 --iter-time 5000 --use-random luksFormat --type luks1 /dev/sda1

[TODO: Explain options]

Short form of the above command:

# cryptsetup -vyc serpent-xts-plain64 -s 512 -h sha512 -i 5000 --use-random luksFormat --type luks1 /dev/sda1

After entering this command, cryptsetup will display a warning, ask you to confirm the operation, and request a passphrase to use:

This will overwrite data on /dev/sda1 irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/sda1: 
Verify passphrase: 
Key slot 0 created.
Command successful.

3.5 Creating logical volumes inside the LUKS container

Open the LUKS container:

# cryptsetup open /dev/sda1 vault

Note: The name vault is just an example. You can choose whatever you like. Only need to keep it consistent.

First, set up the LUKS container as the “physical volume” for LVM:

# pvcreate /dev/mapper/vault
  Physical volume "/dev/mapper/vault" successfully created.

Create a new volume group:

# vgcreate vg0 /dev/mapper/vault
Volume group "vg0" successfully created

Note: The volume group may be named freely. A good option is to use the future hostname of your Alpine system instead of vg0.

Now you'll need to set up some logical volumes according to your desired partitioning scheme with lvcreate, e.g., like this:

# lvcreate -L 2G vg0 -n swap
  Logical volume "swap" created.
# lvcreate -L 15G vg0 -n root
  Logical volume "root" created.
# lvcreate -l 100%FREE vg0 -n home
  Logical volume "home" created.

-l 100%FREE means: Use 100% of the remaining storage space for the volume group.

3.6 Formatting logical volumes

# mkswap -L alpine-swap /dev/vg0/swap
# mkfs.ext4 -L alpine-root /dev/vg0/root
# mkfs.ext4 -L alpine-home /dev/vg0/home

If you prefer to use Btrfs instead of Ext4, first run modprobe btrfs and then replace mkfs.ext4 with mkfs.btrfs.

Now you can activate the swap partition for use during the installation process:

# swapon /dev/vg0/swap

Note: If you need to, you can shut the computer down for now and do the actual installation later. You would only have to do the following after booting from the installation medium next time: install the required packages again (see 3.1), open the LUKS container (see 3.5), activate the LVM volumes by running lvchange -a y vg0, enable swap (see 3.6), and maybe set your keymap prior to doing any of that (see 3.).

4. Installing Alpine

Note: This section of the guide requires network access.

4.1 Setting timezone, hostname, devd for bootstrapping

The latter sets up Busybox' device manager (mdev) just for bootstrapping (-C).

4.2 Setting a root password

Simply run passwd without arguments.

4.3 Cable network

Configure networking devices:

# setup-interfaces
Available interfaces are: eth0 wlan0.
Enter '?' for help on bridges, bonding and vlans.
Which one do you want to initialize? (or '?' or 'done') [eth0]
Ip address for eth0? (or 'dhcp', 'none', '?') [dhcp]
Available interfaces are: wlan0.
Enter '?' for help on bridges, bonding and vlans.
Which one do you want to initialize? (or '?' or 'done') [wlan0] done
Do you want to do any manual network configuration? (y/n) [n]

Add the networking service to runlevel boot:

# rc-update add networking boot

4.4 Setting up additional system services

# rc-update add seedrng boot
 * service seedrng added to runlevel boot 
# rc-update add crond default
 * service crond added to runlevel default
# rc-update add acpid default
 * service acpid added to runlevel default

Start all services added to runlevels boot and default:

# openrc boot
# openrc default

4.5 Setting up NTP

# setup-ntp
Which NTP client to run? ('busybox', 'openntpd', 'chrony' or 'none') [chrony]
100% ████████████████████████████████████████████████████████████
 * service chronyd added to runlevel default 
 * Caching service dependencies ... ok  
 * Starting chronyd ... ok  

4.6 Configuring APK repositories

# setup-apkrepos

Available mirrors:
r) Add random from the above list
f) Detect and add fastest mirror from above list
e) Edit /etc/apk/repositories with text editor

Enter mirror number (1-72) or URL to add (or r/f/e/done) [1] 5
Added mirror
Updating repository indexes... done.

4.7 Configuring the actual device mapper


I chose mdevd.

When asked whether you'd like to scan the hardware to populate /dev, simply go with the default answer: n. (We've already run this script.)

4.8 Setting up sshd (optional)

# setup-sshd

4.9 Installing Alpine on the pre-configured hard drive

4.9.1 Mount file systems

# mount /dev/vg0/root /mnt
# mkdir -p /mnt/home
# mount /dev/vg0/home /mnt/home

4.9.2 Use setup-disk to copy files

# setup-disk -m sys /mnt
100% ████████████████████████████████████████████████████████████
Installing system on /dev/mapper/vg0-root:
/mnt/boot is device /dev/mapper/vg0-root
==> initramfs: creating /boot/initramfs-lts
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-lts
Found initrd image: /boot/initramfs-lts
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
/boot is device /dev/mapper/vg0-root
You might need fix the MBR to be able to boot

4.10 Setting up GRUB

4.10.1 chroot

Mount essentials for chroot environment:

# mount -t proc /proc /mnt/proc 
# mount --rbind /dev /mnt/dev
# mount --make-rslave /mnt/dev
# mount --rbind /sys /mnt/sys

Change root to /mnt:

# chroot /mnt

Adjust the prompt to remind yourself you're in a chroot environment:

# export PS1="[chroot] $PS1"

4.10.2 Configuring GRUB

Now you'll need to edit /etc/default/grub.

First, obtain the UUID of the partition hosting the LUKS container and append it to that file:

[chroot] / # blkid -s UUID -o value /dev/sda1 >> /etc/default/grub

Use this UUID as the value for cryptroot=UUID=uuid as shown below.

The file now needs to contain the following (only with a different UUID):

GRUB_CMDLINE_LINUX_DEFAULT="modules=sd-mod,usb-storage,ext4 cryptroot=UUID=283ca385-4c57-40f1-842b-6beda9c3daf1 cryptdm=vault cryptkey quiet rootfstype=ext4"

If you use Btrfs on your root partition, replace all instances of ext4 in GRUB_CMDLINE_LINUX_DEFAULT with btrfs.

The cryptkey parameter ensures that you won't have to enter the passphrase to unlock the LUKS container twice during boot.

4.10.3 Generating a key file for the initial root file system

As /boot will be encrypted on the installed system, the key to unlock the encrypted container must be included in the initial root file system used by the bootloader.

To create a key file to be picked up by mkinitfs later, run the following commands:

[chroot] / # dd bs=512 count=4 if=/dev/random of=/crypto_keyfile.bin
4+0 records in
4+0 records out
[chroot] / # chmod 000 /crypto_keyfile.bin
[chroot] / # cryptsetup luksAddKey /dev/sda1 /crypto_keyfile.bin
Enter any existing passphrase:
[chroot] / #

Note: The name and location of the key file must be exactly the same as in the above commands.

4.10.4 Creating the initial root file system

Look at /etc/mkinitfs/mkinitfs.conf:

[chroot] / # cat /etc/mkinitfs/mkinitfs.conf
features="ata base ide scsi usb virtio ext4 lvm cryptsetup keymap"

Edit the file: Add cryptkey to features=.

Generate initfs:

mkinitfs -c /etc/mkinitfs/mkinitfs.conf kernel_version

Make sure you specify the kernel version of the system that is going to be installed to disk. It might be different from the one used by the installation medium. When in doubt, just use the directory with the latest kernel version in its name from /lib/modules.

[chroot] / # ls /lib/modules
6.1.30-0-lts  6.1.30-1-lts  6.1.31-0-lts
[chroot] / # mkinitfs -c /etc/mkinitfs/mkinitfs.conf 6.1.31-0-lts
==> initramfs: creating /boot/initramfs-lts

4.10.5 Installing GRUB

[chroot] / # grub-install /dev/sda
Installing for i386-pc platform.
Installation finished. No error reported.
[chroot] / # grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-lts
Found initrd image: /boot/initramfs-lts
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.

4.11 Enabling swap for the newly installed system

In order to make the system activate the drive's swap partition automatically while booting, you'll have to put an entry for that partition into /etc/fstab and add the swap service to runlevel boot.

Add swap partition entry to /etc/fstab:

[chroot] / # printf 'UUID=%s  swap  swap  defaults  0 0\n' "$(blkid -s UUID -o value /dev/vg0/swap)" >> /etc/fstab

Add swap service to runlevel boot:

[chroot] / # rc-update add swap boot
 * service swap added to runlevel boot 

4.12 Creating a normal user

[chroot] / # setup-user -a -g 'audio video netdev'
Setup a user? (enter a lower-case loginname, or 'no') [no] alf
Full name for user alf [alf]
Changing password for alf
New password:
Retype password:
passwd: password for alf changed by root
Enter ssh key or URL for alf (or 'none') [none]
(1/1) Installing doas (6.8.2-r4)
100% ███████████████████████████████████████████████████████████ 
Executing busybox-1.36.0-r9.trigger
OK: 45 MiB in 94 packages

Option -a here adds the user to the wheel group, which is required for using the doas command. Then, -g 'audio video netdev' adds the user to those groups as well.

If the machine you're installing Alpine on has an optical drive, you'll also have to add yourself to the cdrom group in order to access that drive without superuser privileges later. Beware though, that this won't enable you to mount an optical disc as a normal user. But it allows for playing audio or video discs and using the eject command, for example.

Finally, reboot:

[chroot] / # reboot

5. Post-installation steps

5.1 Setting the TTY font

Install some reasonable TTY font, e.g.:

$ doas apk add font-terminus

Installed TTY fonts are located in /usr/share/consolefonts.

Use setfont to test fonts, e.g.:

$ setfont /usr/share/consolefonts/ter-224n.psf.gz

To make this a permanent setting, add consolefont="ter-224n.psf.gz" to /etc/conf.d/consolefont and comment out the default setting.

To have this font setting loaded during boot, add the consolefont service to runlevel boot:

$ doas rc-update add consolefont boot
 * service consolefont added to runlevel boot

5.2 Adding additional software repositories

Alpine Linux mirrors generally offer three kinds of repositories: main, community, and testing. The main repository includes only packages that are officially supported by Alpine. The community repository provides additional packages that have been tested to work on Alpine. Finally, testing, as the name suggests, is for packages that have issues or need testing. This repository is thus only available on the edge branch of Alpine, not on its stable releases.

After installation, the repository list in /etc/apk/repositories already contains a line for the community repository that you just need to uncomment by removing the leading hash sign (#) to enable APK to make use of it. Using this repository in addition to main provides a much wider variety of software. To name just three examples, community includes the MPV media player, the w3m web browser, and the HexChat IRC client.

In order to be able to find software in the community repository and install it, APK needs to re-read the repository list and get the respective index file. This is easily achieved by running doas apk update.

5.3 Installing additional software

[TODO] Introduce basic APK commands.

One of the first things you'll probably want to add to the newly installed system is documentation. The reason this step is necessary is, first, that Alpine splits off most software documentation (including manual pages) into sub-packages with the -doc suffix (e.g., bash-doc for bash) in order to achieve a higher package granularity and keep the main packages small. Second, the system's core manual pages (mostly documenting the kernel and C library interfaces) reside in the man-pages package, which is not installed by default. Third, some tools needed to make use of manual pages are not installed by default either.

To obtain documentation and the tools to use it, install the following packages:

$ doas apk add docs mandoc mandoc-apropos man-pages

What these are for:

Now, what software to install on top of that depends very much on personal needs and preferences. What follows are therefore mere suggestions to give you an idea of what's available and might be useful:

To explore other available software, you can use APK's search, list and info commands. Run man apk for more information.

5.4 Setting up sound

The sound setup on Linux commonly involves PipeWire these days and Alpine can be set up to use it as well. For a simple setup, however, installing alsa-lib and alsa-utils and then adjusting the levels and unmuting Master in alsamixer is sufficient.

[TODO] Document PipeWire setup.

5.5 Setting up a graphical environment

There's more than one way to do it. This is one way to do it.

setxkbmap -layout de &
xrdb -load ~/.Xresources &
xsetroot -solid "#2F4F4F" &
exec openbox-session

Finally, run startx.

You'll probably want to install some additional software, such as a graphical e-mail or IRC client, a media player, etc.

[TODO] Document setup-desktop. Elaborate on “manual” setup. Document Wayland setup.

5.6 Configuring WiFi

There are several ways to set up WiFi on Alpine. This section presents a rather traditional method, using wireless-tools, wpa_supplicant, and the /etc/network/interfaces file.

First, let's use apk add to install the required software, namely:

Installing the linux-firmware package will most likely pull in the required drivers for your WiFi device. However, linux-firmware being a meta-package, it will simply install all Linux firmware packages that are available on Alpine. That might not be what you want. For example, the machine used for the test installations for this guide merely required the linux-firmware-i915 package to make use of its Intel WiFi adapter. So, in this case, the apk command looked like this:

$ doas apk add wireless-tools wpa_supplicant linux-firmware-i915

In order to know which firmware package is needed, first find out about the kind of WiFi adapter on your machine. Filtering the output of lspci or lsusb for things like "wifi", "wireless", "wlan", "network", or "ethernet" with grep -i should help accomplish that. Then find the appropriate firmware package for the device.

Next, list available WiFi interfaces by running iwconfig:

$ iwconfig 
lo        no wireless extensions.

eth0      no wireless extensions.

wlan0     IEEE 802.11  ESSID:off/any  
          Mode:Managed  Access Point: Not-Associated   Tx-Power=off   
          Retry short limit:7   RTS thr:off   Fragment thr:off
          Power Management:off

Scan for WiFi networks to confirm the AP is there (unless it's hidden), or if you don't remember the SSID exactly:

$ iwlist scan

Or, using a pager:

$ iwlist scan | less

Now use wpa_passphrase to create an appropriate network stanza in /etc/wpa_supplicant/wpa_supplicant.conf. Because you need to redirect output to a file only root can write to here, you'll have to use su to get a root shell for this part.

$ su
# wpa_passphrase 'essid' >> /etc/wpa_supplicant/wpa_supplicant.conf
# reading passphrase from stdin
<Type password here. Then hit Enter.>
# exit

The result in the file should look like this, except for the values:


By the way, make sure access to /etc/wpa_supplicant/wpa_supplicant.conf is thoroughly restricted. No one except root should be able to even read that file. This is easily achieved by running

$ doas chmod 000 /etc/wpa_supplicant/wpa_supplicant.conf

Also, when WPA access has been tested to work, the line containing the actual WPA password is better removed.

Test WPA access:

$ doas wpa_supplicant -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf

If it works, start wpa_supplicant in the background:

$ doas wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf

Now let wlan0 acquire an IP address on the local network:

$ doas udhcpc -i wlan0
udhcpc: started, v1.36.1
udhcpc: broadcasting discover
udhcpc: broadcasting discover
udhcpc: broadcasting select for, server
udhcpc: lease of obtained from, lease time 3600

Test network access by, e.g., visiting Alpine's website in your prefered browser.

To set up the wlan0 interface permanently, add the following to /etc/network/interfaces:

auto wlan0
iface wlan0 inet dhcp
  wifi-config-path /etc/wpa_supplicant/wpa_supplicant.conf

If you don't want the system to try to bring up the WiFi interface during boot, comment the line saying auto wlan0, i.e., make it #auto wlan0. I highly recommend doing that. You can bring the interface up with a simple doas ifup wlan0 as needed. This also goes for the Ethernet interface, of course.

Now stop wpa_supplicant if it's still running. Then run doas ifdown wlan0 && doas ifup wlan0 to test the configuration in /etc/network/interfaces. If it works, WiFi setup is done.

[TODO] Elaborate on why commenting auto lines is recommended.

[TODO] Explain how to set up automatic reconnecting on flaky connections.

[TODO] Explain how to connect to open WiFi networks.


Much of the information presented here was extracted from the Alpine Linux Wiki and sorted out and brushed up with the help of several people on the #alpine-linux IRC channel at OFTC that are far more knowledgeable about Alpine Linux (and Linux in general) than me. I also took a lot of inspiration from a similar guide published by Sumit Khanna in 2020.

Last changed: 2023-07-30

Copyright © 2023 Michael Siegel

This work is made available under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0).