Shrinking an EC2 Root Volume
How to safely shrink an AWS root volume below the base AMI snapshot limit on a live system.
AWS forcibly restricts you from provisioning a root volume smaller than the base AMI's snapshot (typically 8 GB). If you want to squeeze a Debian instance into a 3 GB drive to enforce hard storage quotas or save on EBS costs, you have to do it by hand.
Groundwork
This guide is executed on a Graviton (arm64) instance running Debian 13 with UEFI boot. If you are on x86_64, the process is identical, but your GRUB target will be x86_64-efi instead of arm64-efi.
| Item | Value |
|---|---|
| Instance | t4g.micro (Graviton, arm64/aarch64) |
| Boot | UEFI + GRUB 2 (arm64-efi) via shim |
| Target Build | 3 GB total: 512 MiB ESP (FAT32) + ~2.5 GiB ext4 root |
| New Disk ID | /dev/nvme4n1 (Always verify yours with lsblk) |
Before touching the terminal, go to the AWS Console, create a new 3 GB gp3 volume in the same Availability Zone, and attach it to your instance.
Important: All commands below assume a root shell (sudo -i) and must be run in a continuous session so the environment variables persist.
# Find the new, empty 3G disk
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS
# Ensure your current system actually fits in ~2.4 GiB
df -h /
# Set the target disk variable (change to match your lsblk output)
DISK=/dev/nvme4n1 The Migration Process
- Partition and Format the Target: UEFI requires a dedicated FAT32 boot partition. Why two partitions? The instance boots via UEFI, and UEFI refuses to load a bootloader from anything other than a dedicated EFI System Partition (ESP). A single ext4 partition will flatly fail to boot. I use GPT with a 1 MiB offset to ensure perfect partition alignment.
apt-get update && apt-get install -y parted dosfstools
parted -s $DISK mklabel gpt
parted -s $DISK mkpart primary fat32 1MiB 513MiB # Partition 1: ESP (512 MiB)
parted -s $DISK set 1 esp on # Flag as EFI System Partition
parted -s $DISK mkpart primary ext4 513MiB 100% # Partition 2: Root
# Format the partitions
mkfs.fat -F 32 ${DISK}p1
mkfs.ext4 ${DISK}p2
# Mount the new root (ESP gets mounted later)
mkdir -p /mnt/root
mount ${DISK}p2 /mnt/root
mkdir -p /mnt/root/boot/efi- Clone the Live Root Filesystem: Strict boundary rules prevent recursion. The goal is a faithful, bit-for-bit clone of the operating system without recursively copying attached network drives or virtual file systems into themselves.
rsync -aHAX --one-file-system \
--exclude='/dev/*' --exclude='/proc/*' --exclude='/sys/*' --exclude='/run/*' \
--exclude='/tmp/*' --exclude='/mnt/*' --exclude='/media/*' \
--exclude='/lost+found' --exclude='/swapfile' \
/ /mnt/root/-aHAX: Maintains permissions, owners, hard links, ACLs, and extended attributes.--one-file-system: Stopsrsyncdead at mount boundaries.--exclude='/x/*': Skips the contents of the directories but keeps the empty directories themselves. Excluding/deventirely would delete the mountpoint and break the chroot environment later.
Because /mnt/* was excluded, you must manually recreate any custom mountpoints your system expects under /mnt on the target drive (e.g., mkdir -p /mnt/root/mnt/postgres).
- Rewire the Mount Points: Updating /etc/fstab to use the new UUIDs.
The
rsynccommand faithfully cloned the oldfstab, which means the new drive is currently configured to look for the old drive's partitions. Debian cloud images usePARTUUID, so we swap the old IDs for the new ones usingsed.
NEW_ROOT=$(blkid -s PARTUUID -o value ${DISK}p2)
NEW_ESP=$(blkid -s PARTUUID -o value ${DISK}p1)
OLD_ROOT=$(blkid -s PARTUUID -o value $(findmnt -no SOURCE /))
OLD_ESP=$(blkid -s PARTUUID -o value $(findmnt -no SOURCE /boot/efi))
sed -i "s/$OLD_ROOT/$NEW_ROOT/; s/$OLD_ESP/$NEW_ESP/" /mnt/root/etc/fstab
cat /mnt/root/etc/fstabVerify the output. Leaving an old ID in place guarantees a kernel panic on boot.
- Inject the UEFI Bootloader: Chroot execution with standard fallback.
To install GRUB correctly, we have to fake a live environment for the new disk by bind-mounting the virtual file systems. We also pass through DNS (
resolv.conf) so package managers work inside the jail if needed.
mount ${DISK}p1 /mnt/root/boot/efi
mount --bind /dev /mnt/root/dev
mount --bind /dev/pts /mnt/root/dev/pts
mount --bind /proc /mnt/root/proc
mount --bind /sys /mnt/root/sys
mount -t tmpfs tmpfs /mnt/root/run
mount --bind /etc/resolv.conf /mnt/root/etc/resolv.conf
chroot /mnt/root /bin/bashInside the chroot jail, install GRUB targeting the Graviton architecture. I explicitly pass --force-extra-removable. This writes a copy of the bootloader to the fallback path (EFI/BOOT/BOOTAA64.EFI). Without this, the EC2 instance's firmware has no NVRAM entry pointing to the new disk and will drop you straight into a UEFI shell on boot.
grub-install --target=arm64-efi --efi-directory=/boot/efi --bootloader-id=debian --force-extra-removable
update-grub
exit
sync
umount -R /mnt/root
poweroff-
Execute the Hot Swap: AWS Console manipulation. Once the instance state shows Stopped in the AWS Console, sever the old drive and promote the new one:
-
Go to Instance → Storage and note your exact Root device name (e.g.,
/dev/xvda). -
Detach the original 8 GB volume. (Keep it as your rollback insurance).
-
Detach the new 3 GB volume from its temporary
/dev/sdfmounting point. -
Attach the 3 GB volume back to the instance, ensuring the Device name exactly matches the root device name you noted in step 1. If you use a different name, AWS will not treat it as a boot disk.
-
Start the instance.
Emergency Recovery: The UEFI Shell Loop
If you skipped the --force-extra-removable flag in Step 4, the instance will boot up, fail its status checks, and drop into an infinite loop in the EC2 Serial Console:
The system will reboot in 48 seconds. Press any key to run interactive shell.
Shell>This happens because the firmware's boot entries still point at the old volume's ESP. The files exist on your new disk, but the UEFI NVRAM doesn't know where to look. Press any key to interrupt the countdown and map the bootloader manually:
Shell> map -r # Refresh device list. FS0: should be the new FAT32 ESP.
Shell> FS0:
FS0:\> ls EFI\debian\ # Verify shimaa64.efi exists (use backslashes!)
FS0:\> bcfg boot dump # View stale entries
FS0:\> bcfg boot add 0 FS0:\EFI\debian\shimaa64.efi "Debian"
FS0:\> exit # RebootThis injects the correct boot mapping into position 0 (highest priority). The instance will immediately reboot and launch normally.