Manually Installing Ubuntu 22.04 on Surface Go (with Hibernation)

Just a few months ago I had a post about installing Ubuntu 22.04 on Surface Go. And that guide is still valid. However, while using my Surface Go during vacation, I noticed I miss hibernation. Yes, deep sleep is nice enough but hibernation is much sweeter deal when you expect your device to wake up after a longer time period.

Do note I am a huge fan of encryption and thus this guide will have both data and swap encrypted thus complicating setup a bit.

As always, all starts with a creation of Ubuntu installation media and booting into it. If done from Windows, you can use the original instructions for both. If you already have Linux installed, you can check how to do it from grub. Either way, I'll asume you have it all booted and that you selected "Try Ubuntu" when offered.

From there we need to get into Terminal and become a root user.

sudo -i

The very next step should be setting up a few variables - host, user name, and disk. This way we can use them going forward and avoid accidental mistakes.


Disk setup is a bit more wasteful than what you would get following the original guide. While EFI and boot partitions are the same size, the swap partition has been increased to match RAM size (4 GB in my case). While you don't necessarily need it that big, it will help with hibernation if you do. And yes, I'm cheating a bit since the final swap size will be a bit under as encryption headers will take a bit of space. If you really need all MMC space you can get, system will work (most of the time) fine with 2 GB too.

blkdiscard -f $DISK
sgdisk --zap-all                     $DISK
sgdisk -n1:1M:+63M -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+640M -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+4G   -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:0     -t4:8309 -c4:Root $DISK
sgdisk --print                       $DISK

While one could encrypt boot partition too, I usually don't do it as it prevents double prompt (grub has to unlock boot partition separately of others) and that is too annoying for my taste. I do encrypt both data and swap of course. Make sure they use the same password if you don't want to always enter password twice.

cryptsetup luksFormat -q --type luks2 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i ${DISK}p4

cryptsetup luksFormat -q --type luks2 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i ${DISK}p3

Once encryption is done, we need to load the devices. For the main partition I like to use hostname as it's displayed in the prompt.

cryptsetup luksOpen ${DISK}p4 ${HOST}

cryptsetup luksOpen ${DISK}p3 swap

Now we can format (and mount) all partitions.

yes | mkfs.ext4 /dev/mapper/${HOST}
mkdir /mnt/install
mount /dev/mapper/${HOST} /mnt/install/

mkswap /dev/mapper/swap

yes | mkfs.ext4 ${DISK}p2
mkdir /mnt/install/boot
mount ${DISK}p2 /mnt/install/boot/

mkfs.msdos -F 32 -n EFI -i 4d65646f ${DISK}p1
mkdir /mnt/install/boot/efi
mount ${DISK}p1 /mnt/install/boot/efi

To start the fun we need the debootstrap package. Do make sure you have Wireless network connected at this time as otherwise operation will not succeed.

apt update ; apt install --yes debootstrap

And then we can get basic OS on the disk. This will take a while.

debootstrap $(basename `ls -d /cdrom/dists/*/ | grep -v stable | head -1`) /mnt/install/

Our newly copied system is lacking a few files and we should make sure they exist before proceeding.

echo $HOST > /mnt/install/etc/hostname
sed "s/ubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
sed '/cdrom/d' /etc/apt/sources.list > /mnt/install/etc/apt/sources.list
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

If you are installing via WiFi, you might as well copy your wireless credentials

mkdir -p /mnt/install/etc/NetworkManager/system-connections/
cp /etc/NetworkManager/system-connections/* /mnt/install/etc/NetworkManager/system-connections/

Finally we're ready to "chroot" into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install \
    /usr/bin/env DISK=$DISK HOST=$HOST USER=$USER \
    bash --login

For the new system we need to setup the locale and the time zone.

locale-gen --purge "en_US.UTF-8"
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US
dpkg-reconfigure --frontend noninteractive locales

dpkg-reconfigure tzdata

Now we're ready to onboard the latest Linux image. And yes, there is a Surface Go specific kernel, but it seems that 5.17 you get with OEM kernels is as good.

apt update
apt install --yes --no-install-recommends linux-oem-22.04

Then we install boot environment packages.

apt install --yes initramfs-tools cryptsetup keyutils grub-efi-amd64-signed shim-signed

Since we're dealing with encrypted data, we should auto mount it via crypttab. If there are multiple encrypted drives or partitions, keyscript really comes in handy to open them all with the same password..

echo "${HOST} UUID=$(blkid -s UUID -o value ${DISK}p4)  none \
    luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "swap UUID=$(blkid -s UUID -o value ${DISK}p3) none \
    swap,luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
cat /etc/crypttab

To mount partitions, we need to do some fstab setup too:

echo "UUID=$(blkid -s UUID -o value /dev/mapper/${HOST}) \
    / ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value ${DISK}p2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value ${DISK}p1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "UUID=$(blkid -s UUID -o value /dev/mapper/swap) \
    none swap defaults 0 0" >> /etc/fstab
cat /etc/fstab

While defaults are actually matching most of the needed values, I like to explicilty list them and manually configure the time after which sleep will be followed by automatic hybernation (10 minutes in my case).

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=10min/' \

Lid switch can be then easily setup to use suspend-then-hibernate.

sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=suspend-then-hibernate/'

I also like to adjust swappiness to make system a bit more lively.

echo "vm.swappiness=10" >> /etc/sysctl.conf

With all that out of way, we can finally update our boot environment.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -u -k $KERNEL

Grub update is the last part we need to make system bootable. And no, the second initramfs update is not optional as it needs to pickup RESUME variable.

    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/swap)\"/" \
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy
update-initramfs -u -k all

Finally we install out GUI environment. I personally like ubuntu-desktop-minimal but you can opt for ubuntu-desktop.

apt install --yes ubuntu-desktop-minimal

Since this will not install any browser, you can add Firefox package too (apt install firefox) but I like to download Chrome.

cd /tmp
wget --inet4-only
sudo apt install ./google-chrome-stable_current_amd64.deb

Having power button do a hibernate action requires a bit more effort. And while you can use suspend-then-hibernate here too, I personally prefer to have it linked to straight hibernate.

gsettings set org.gnome.settings-daemon.plugins.power power-button-action nothing
echo 'event=button/power' > /etc/acpi/events/power
echo 'action=sudo systemctl hibernate' >> /etc/acpi/events/power

Short package upgrade will not hurt.

add-apt-repository universe
apt update ; apt dist-upgrade --yes

The only remaining task before restart is to create the user, assign a few extra groups to it, and make sure its home has correct owner.

adduser --disabled-password --gecos '' -u 1002 $USER
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

As install is ready, we can exit our chroot environment.


And now we can unmount our disk, followed by a reboot.

umount /mnt/install/boot/efi
umount /mnt/install/boot
mount | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}


If everything went fine, we can now hibernate.

systemctl hibernate

If you see hibernate not supported message, turn off the secure boot. At this time hibernation is not supported when secure boot is turned on.

Leave a Reply

Your email address will not be published. Required fields are marked *