9

When installing Raspbian, the first thing you're told to do is to expand the file system to stretch across the whole SD card. Works great. But what if I want to save space when doing a disk image. Can I reverse the expansion, and end up with a small disk image which I can use to copy my system to other SD cards and then expand it again on them?

Paolo Vacirca
  • 233
  • 2
  • 3
  • 6

4 Answers4

11

[If all you want to do is shrink the partition on the card, there is a simpler way to do this than explained here by first using resize2fs on the filesystem, then using fdisk to shrink the partition; there should be many examples of this around online, e.g., at Unix & Linux. The method below involves creating a new, smaller card image. Either way, you cannot do this to a running system.]

You can't shrink a device image (although you can shrink an individual filesystem with resize2fs), but you can create one of whatever size and duplicate the contents of another one. I'll discuss this in relation to Raspbian but it is the same for any other GNU/Linux system.

A device image is for our purposes a complete byte for byte copy of a bootable device medium such an SD card, which may contain zero or more filesystem partitions. This is distinct from a filesystem image, which is a byte for byte copy of a single partition. See here for more about these important distinctions.

This answer describes how to create a device image with filesystem partitions in it, but those partitions will be empty. You then need to copy the contents of each partition in as per my other answer here. This is the most flexible method.

Some of these commands need to be run root so I recommend you just su root. You could use the pi itself to do this if you have storage big enough attached.

There are two partitions in the Raspbian image, the first small boot partition and the second larger root filesystem. There is also a partition table, aka. the master boot record (MBR), since this is a device image. The MBR is critical and occupies the first 512 bytes in the image.

A device image starts as an empty file. You need to decide how big you want it to be; du -h and df may be useful in this regard (see man du and man df; all the commands below also have man pages worth at least a glance).

I'll use a small one as an example.

dd if=/dev/zero of=test.img bs=4096 count=10240

That's ~40 MiB; the total size is determined by bs * count, the default unit is bytes. This empty file can now be treated as a device.

fdisk test.img

Fdisk is interactive and will note that the "device does not contain a recognized partition table". The first thing you have to do is create one. An MBR formatted device uses a DOS partition table, and the fdisk command to create that is o.

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

You can see the empty table with p. We now need to create the two new partitions with n.

Command (m for help): n

Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): p
Partition number (1-4, default 1): 1
First sector (2048-79999, default 2048): 8192

Note I did not use the default value; 8192 is what's used in the actual Raspbian images and seems to be a common practice with SD cards.1 In the Raspbian image this partition is ~56 MB and so ends at sector 122879. It does not have to be that big but you might as well duplicate the pattern. In this example, though, there isn't enough space, so I used 10 MB:

Last sector, +sectors or +size{K,M,G,T,P} (8192-79999, default 79999): +10M

By default the type of the partition is "Linux". We need to change that.

Command (m for help): t

Selected partition 1
Hex code (type L to list all codes): c

Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'.

Now the second partition.

Command (m for help): n

Partition type:
   p   primary (1 primary, 0 extended, 3 free)
   e   extended
Select (default p): 

Using default response p.
Partition number (2-4, default 2): 2
First sector (2048-79999, default 2048): 28672

Notice again I didn't use the default; because we left empty space at the beginning, fdisk still wants to use it. Instead I used the sector2 number after the last sector of the first partition, which you can find in the partition table (it's the End).

For the last sector I chose the highest possible value (79999) to fill the entire image file.

Once that's created the table looks like this:

Device    Boot     Start       End Blocks  Id System
test.img1           8192     28671  10240   c W95 FAT32 (LBA)
test.img2          28672     79999  25664  83 Linux

That's all we need here, but to hedge against mistakes, fdisk doesn't really do anything until you press w. At that point it will write out the new table and exit. To check:

> file test.img 
test.img: ; partition 1 : ID=0xc, start-CHS (0x0,130,3), end-CHS     (0x1,200,7), startsector 8192, 20480 sectors; partition 2 : ID=0x83, start-CHS (0x1,200,8), end-CHS (0x4,249,53), startsector 28672, 51328 sectors

It's recognized as a device with filesystem partitions. Those need to be formatted. To do this, we need to indicate appropriate regions of the image file as filesystems to the OS. First:

losetup -o 4194304 /dev/loop1 ./test.img
losetup -o 14680064 /dev/loop2 ./test.img

Those offset (-o) values are the Start numbers from the partition table, 8192 and 28672, times the unit size (512). Now we have device partitions that can be formatted:

> mkfs.vfat -F32 /dev/loop1
mkfs.fat 3.0.27 (2014-11-12)
Loop device does not match a floppy size, using default hd params
> mkfs.ext4 /dev/loop2

That warning about floppy size can be ignored. The second command will produce more output but there should not be any errors reported. We can now check the filesystems; first discard the loop devices with losetup -D, then create a couple of directories and mount the image partitions there:

mkdir one; mkdir two
mount -o offset=4194304 -t vfat test.img one
mount -o offset=14680064 -t ext4 test.img two

This should not report any errors (if you get "overlapping loop", see here and search the page for "overlapping loop"). one will be empty, and two should have a lost+found directory in it.

You can now copy whatever you want into these mount points, e.g., you could duplicate a pi using rsync, as per that other Q&A linked earlier. When you're done, umount the directories and you can dd test.img onto an SD card and use it.


1. If you are trying to duplicate some other image/card than a Raspbian one, you can get these numbers by looking at the image/card's partition table the same way with fdisk.

2. Generally the units used by fdisk are indicated when you view the partition table (e.g. Units: sectors of 1 * 512 = 512 bytes) but beware that it may also refer to Blocks in the table (as above) which are 1 KiB. This can be confusing because the Start and End figures aren't in "blocks", they're in units indicated at the top. Here those are 1/2 KiB, hence, the distance between those figures is twice the number of Blocks. Not all versions of fdisk do this, some report Sectors, which makes more sense.

goldilocks
  • 56,430
  • 17
  • 109
  • 217
2

To shrink an existing image I will give an example with a Raspberry Pi OS Lite image that was backed up from an already used and expanded 32 GB SD Card.

This method is using a computer with a Linux operating system. I use one with Debian Buster installed. If you don't have one available, you can use your RasPi. The method can only shrink the last partition to reduce the image. Shrinking partitions in the middle of the image will free space but does not move the last partition, so you can't cut freed space at the end of the image. Managing this is much more complex. You also have to know that the filesystem is within the partition. The partition can be seen as a container for the filesystem. You can shrink and expand it within the partition and you can shrink the partition as long as it doesn't get smaller than the filesystem and cut data of it.

So we will first shrink the filesystem as much as possible. Then we will shrink the partition a little bit to ensure that it doesn't hurt the filesystem. Then we expand the filesystem to the size of the shortened partition and cut off the freed space at the end of the image. We need five utilities to do that:

  • parted - to manage the partitions on the image
  • losetup - to access the filesystems within the partitions on the image
  • fsck - to check the filesystem
  • resize2fs - to resize the filesystem
  • dd - to cut the freed space at the end of the image

I will reduce the image by 100 MiB so that it will fit onto a somewhat smaller 30 GiB (32 GB) SD Card. I use the binary system of units (e.g. MiB based on 1024) and not the decimal one (e.g. MB based on 1000) because this is the "natural" system of parted for partition boundaries. So please have attention to the units.

This is the starting situation of the image:

rpi ~$ sudo parted raspios.img unit MiB print free
Model:  (file)
Disk /mnt/backup/raspios.img: 30437MiB
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:

Number  Start    End       Size      Type     File system  Flags
        0,02MiB  4,00MiB   3,98MiB            Free Space
 1      4,00MiB  260MiB    256MiB    primary  fat32        lba
 2      260MiB   30436MiB  30177MiB  primary  ext4

First make the filesystems available as device files:

rpi ~$ sudo losetup --show --find --partscan raspios.img
/dev/loop0
rpi ~$ ls /dev/loop0*
/dev/loop0  /dev/loop0p1  /dev/loop0p2

Of course, the filesystem in the last partition is /dev/loop0p2 and we must check it before shrinking:

rpi ~$ sudo fsck -f /dev/loop0p2
fsck from util-linux 2.33.1
e2fsck 1.44.5 (15-Dec-2018)
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
rootfs: 44587/1888000 files (0.1% non-contiguous), 447590/7725184 blocks

Shrink the filesystem as much as possible, then detach all filesystems, because we modify the partition:

rpi ~$ sudo resize2fs -M /dev/loop0p2
resize2fs 1.44.5 (15-Dec-2018)
Resizing the filesystem on /dev/loop0p2 to 485647 (4k) blocks.
The filesystem on /dev/loop0p2 is now 485647 (4k) blocks long.

rpi ~$ sudo losetup --detach-all

Now reduce the last partition from its end at 30436MiB (see starting situation above) to the new end at 30336MiB to make it 100MiB smaller.

rpi ~$ sudo parted raspios.img resizepart 2 30336MiB
Warning: Shrinking a partition can cause data loss, are you sure you want to continue?
Yes/No? Yes

rpi ~$ sudo parted raspios.img unit MiB print free
Model:  (file)
Disk /mnt/backup/raspios.img: 30437MiB
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:

Number  Start     End       Size      Type     File system  Flags
        0,02MiB   4,00MiB   3,98MiB            Free Space
 1      4,00MiB   260MiB    256MiB    primary  fat32        lba
 2      260MiB    30336MiB  30076MiB  primary  ext4
        30336MiB  30436MiB  100MiB             Free Space

Very nice, we have 100MiB Free Space at the end now. Just attach the filesystem again and expand it to the new partition size.

rpi ~$ sudo losetup --show --find --partscan raspios.img
rpi ~$ sudo resize2fs /dev/loop0p2
resize2fs 1.44.5 (15-Dec-2018)
Resizing the filesystem on /dev/loop0p2 to 7699456 (4k) blocks.
The filesystem on /dev/loop0p2 is now 7699456 (4k) blocks long.

rpi ~$ sudo losetup --detach-all

Now copy the image without the free space at the end to the new SD Card, assuming it is attached to /dev/sdb. To match exactly the partition boundaries I will use sector units. We have to count the sectors until the start of the Free Space, otherwise parted will complain Error: Can't have a partition outside the disk!.

rpi ~$ sudo parted raspios.img unit s print free
Model:  (file)
Disk /mnt/backup/raspios.img: 62333952s
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:

Number  Start      End        Size       Type     File system  Flags
        32s        8191s      8160s               Free Space
 1      8192s      532479s    524288s    primary  fat32        lba
 2      532480s    62128128s  61595649s  primary  ext4
        62128129s  62333951s  205823s             Free Space

rpi ~$ sudo dd if=raspios.img of=/dev/sdb bs=62128129 count=512 conv=fsync

When finished, check if everything is OK:

rpi ~$ sudo parted /dev/sdb unit MiB print free
rpi ~$ sudo fsck -f /dev/sdb1
rpi ~$ sudo fsck -f /dev/sdb2

If you have enough space on your storage you can also store the shrinked image:

rpi ~$ sudo dd if=raspios.img of=raspios-shrinked.img bs=62128129 count=512 conv=fsync
Ingo
  • 40,606
  • 15
  • 76
  • 189
1

There's a handy utility called PIShrink which will shrink a Pi image file - essentially carrrying out the steps mentioned in the other answers.

Pierz
  • 794
  • 9
  • 8
1

I had rather good experience with image-shrink from image-utils:

image-shrink imagefile [Additional MB]
Dmitry Grigoryev
  • 26,688
  • 4
  • 44
  • 133