It has often been said that some distributions are bloated or not as minimal as others like Arch, Gentoo, Void or OpenBSD.

Even though most major distributions, either BSD or Linux, can’t really be considered bloated, it can be argued that some desktop-oriented setups or flavors are far from minimal in their default configurations.

Having said that, nothing really stops a user from making a bare bones install that will take very few resources. This principle applies to most major distributions, which often provide minimal installation processes where the system can be tailored to the user’s needs.

In this post, we will try to get a lightweight system using Debian’s readily minimal installation options.

Installing the base system

Doing a netboot install

The easiest way to get a minimal Debian installation is to use the mini.iso file, commonly known as netboot (a portmanteau of network boot). We can download the latest ISO from Debian’s website:

wget https://deb.debian.org/debian/dists/stretch/main/installer-amd64/current/images/netboot/mini.iso

We will be using QEMU to do the installation, so we pass both the ISO file and the destination device (e.g. /dev/sdi) as arguments to the executable:

qemu-system-x86_64 -m 256 -drive file=mini.iso,media=cdrom -drive file=/dev/sdi,format=raw,cache=none

From here on we can use most of the defaults. We just have to make sure nothing is selected during the Software selection step of the installation process:

Debian software selection

Using debootstrap

An alternative option is to use the package debootstrap to create a bare installation. To install the necessary packages in our local system, we run:

sudo apt install debootstrap

Since there is no installer to help us create the partitions, we must create them manually. We can use GParted, fdisk or any other partitioning tool for this:

printf 'o\nn\np\n1\n\n\nw\n' | sudo fdisk /dev/sdi

We must then create an ext4 file system with the command:

sudo mkfs.ext4 /dev/sdi1
mke2fs 1.43.4 (31-Jan-2017)
Creating filesystem with 61049389 4k blocks and 15269888 inodes
Filesystem UUID: 60bb0786-b320-4285-abc0-95efce9ac10b
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
        4096000, 7962624, 11239424, 20480000, 23887872

Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

Now, let’s mount the partition:

sudo mount /dev/sdi1 /mnt/debian

And use debootstrap to copy the base system:

sudo debootstrap --arch amd64 stretch /mnt/debian http://deb.debian.org/debian

This will take a few minutes since it has to download the required packages and copy them on to the disk.

Configuring the system

Now we will chroot into our newly created system to apply some finishing touches. But before doing this, we need to recreate some basic file system hierarchy:

sudo mount -t proc /proc /mnt/debian/proc && \
sudo mount --rbind --make-rslave /dev /mnt/debian/dev && \
sudo mount --rbind --make-rslave /sys /mnt/debian/sys && \
sudo mount --rbind --make-rslave /run /mnt/debian/run

Now we can safely use chroot:

sudo chroot /mnt/debian /bin/bash

First of all, let’s change the hostname:

echo 'debianlight' > /etc/hostname && \
echo -e '127.0.1.1\tdebianlight' >> /etc/hosts

By default, /etc/fstab is empty, so we must add the disk with the UUID of the file system we created earlier. We can use the command blkid to see it:

blkid
/dev/sdi1: UUID="60bb0786-b320-4285-abc0-95efce9ac10b" TYPE="ext4" PARTUUID="17a30f04-01"

So let’s create the appropriate entry:

cat <<- 'EOF' > /etc/fstab
UUID=60bb0786-b320-4285-abc0-95efce9ac10b / ext4 defaults 0 0
EOF

For Debian’s stable release, updates to stuff like virus scanners or timezone data are delivered via the updates repository. Therefore, it’s a good idea to add it to our sources.list:

cat <<- 'EOF' >> /etc/apt/sources.list
deb http://deb.debian.org/debian stretch-updates main
EOF

We should also make sure to enable the security updates, especially if this system is to be online. For this, we need to add the repository for Debian’s security team:

cat <<- 'EOF' >> /etc/apt/sources.list
deb http://security.debian.org/debian-security stretch/updates main
EOF

Let’s get our system up to date:

apt update && apt upgrade --no-install-recommends

A debootstrap installation is mainly used for chroots or containers, therefore it’s missing a few fundamental packages. To have a bootable system, we must install them before we boot the machine for the first time.

First let’s install and configure the locales to avoid some annoying error messages:

apt install --no-install-recommends locales && dpkg-reconfigure locales

Since no timezone is configured, the time and date may be reported erroneously. Let’s fix that:

dpkg-reconfigure tzdata

Of course, we shouldn’t forget about installing a kernel:

apt install --no-install-recommends linux-image-amd64

Also, we have to install a boot loader on our disk (e.g. /dev/sdi) so the system can be booted:

apt install --no-install-recommends grub-pc && update-grub

And replace all entries in grub.cfg with the UUID for our disk:

sed -i 's,root=/dev/sdi[0-9],root=UUID=60bb0786-b320-4285-abc0-95efce9ac10b,' /boot/grub/grub.cfg

At the moment, we are using our system’s network connection inside the chroot, but this won’t be available once we boot this new system by itself. We need to add a configuration for our network card in the /etc/network/interfaces.d directory. Since we are going to test the installation with QEMU, we can use the default interface’s name:

cat <<- 'EOF' > /etc/network/interfaces.d/ens3
allow-hotplug ens3
iface ens3 inet dhcp
EOF

In order to login as root, we’ll need to set a password:

passwd
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully

Finally, let’s clean up and exit the chroot so we can boot into our new system:

apt clean; exit

Although first we should cleanly unmount the file hierarchy we previously set up:

sudo umount -R /mnt/debian

Booting the system

Now, let’s boot up with QEMU:

qemu-system-x86_64 -m 256 -drive file=/dev/sdi,format=raw,cache=none -net user,hostfwd=tcp::2222-:22 -net nic

Manually entering commands in QEMU’s console is not very efficient (it’s not possible to copy and paste), so it’s always a good idea to install SSH so we can access the system remotely. After logging in as root, we can install it:

apt install --no-install-recommends ssh

We must also enable root access by modifying /etc/ssh/sshd_config:

cat <<- 'EOF' >> /etc/ssh/sshd_config
PermitRootLogin yes
EOF

Once we have rebooted the virtual machine, we should be able to connect from our own machine using SSH:

ssh -p 2222 root@localhost

System resources usage

Let’s see how much of the system’s resources we are using. For instance, for the netboot install:

df -h --output=source,used,target /
Filesystem      Used Mounted on
/dev/sda1       635M /
free -ht | grep ^Total
Total:  240M  26M 168M
dpkg -l | grep ^ii | wc -l
217

And for the debootstrap install:

df -h --output=source,used,target /
Filesystem      Used Mounted on
/dev/sda1       579M /
free -ht | grep ^Total
Total:  240M  26M 169M
dpkg -l | grep ^ii | wc -l
191

Not bad! But we can do better.

Avoiding unnecessary packages

Since we are trying to make this system as minimal as possible, we should make sure only the required packages are installed without having to provide the --no-install-recommends option every time:

cat <<- 'EOF' >> /etc/apt/apt.conf.d/99local
APT::Install-Suggests "0";
APT::Install-Recommends "0";
EOF

Removing some fluff

Now, we can trim some disk space by deleting unused locales. We can just use a one-liner to do this:

find /usr/share/locale -mindepth 1 -maxdepth 1 ! -name 'en*' -exec rm -r {} \;

To prevent packages from installing unwanted locales, we can force dpkg to ignore them:

cat <<- 'EOF' > /etc/dpkg/dpkg.cfg.d/01_nolocales
path-exclude /usr/share/locale/*
path-include /usr/share/locale/en*
EOF

The same thing can be done for documentation files:

find /usr/share/doc -depth -type f ! -name copyright -delete
find /usr/share/doc -empty -delete
rm -rf /usr/share/man /usr/share/groff /usr/share/info /usr/share/lintian /usr/share/linda /var/cache/man

And to prevent them from being installed at all:

cat <<- 'EOF' > /etc/dpkg/dpkg.cfg.d/01_nodocs
path-exclude /usr/share/doc/*
path-include /usr/share/doc/*/copyright
path-exclude /usr/share/man/*
path-exclude /usr/share/groff/*
path-exclude /usr/share/info/*
path-exclude /usr/share/lintian/*
path-exclude /usr/share/linda/*
EOF

We can even get more space by removing some unnecessary packages:

apt purge --auto-remove apt-listchanges aptitude aspell* at avahi-autoipd avahi-daemon bc bluetooth debconf-i18n debian-faq* doc-debian eject exim4-base groff iamerican ibritish info installation-report ispell* krb5-locales logrotate manpages modemmanager nano os-prober pcscd ppp popularity-contest reportbug rsyslog util-linux-locales wamerican

To remove old logs, we can run the following command:

find /var/log -type f -cmin +10 -delete

Final comparison

What does our system looks like after all these improvements? Let’s see how the netboot install fares:

df -h --output=source,used,target /
Filesystem      Used Mounted on
/dev/sda1       544M /
free -ht | grep ^Total
Total:  240M  25M 170M
dpkg -l | grep ^ii | wc -l
202

And for the debootstrap install:

df -h --output=source,used,target /
Filesystem      Used Mounted on
/dev/sda1       503M /
free -ht | grep ^Total
Total:  240M  25M 171M
dpkg -l | grep ^ii | wc -l
187

We can see that the netboot install adds some extra packages that we could easily remove, although the improvement would be marginal:

apt purge --auto-remove busybox discover kbd keyboard-configuration laptop-detect pciutils task-english

Other tweaks

Removing systemd

If we are no fans of systemd and aren’t using any of its features, we can remove it and install a different init system in its place. For this example, we will install SysV:

apt install --purge --auto-remove --no-install-recommends sysvinit-core

Then, let’s create an inittab file:

cp /usr/share/sysvinit/inittab /etc/inittab

After a reboot using the new init system, we can purge systemd:

apt purge --auto-remove systemd libpam-systemd

To avoid installing any systemd package in the future, we will configure APT accordingly:

cat <<- 'EOF' >> /etc/apt/preferences.d/nosystemd
Package: libsystemd0
Pin: release *
Pin-Priority: 500

Package: *systemd*
Pin: release *
Pin-Priority: -1
EOF

Without systemd, we get a slight improvement on memory usage and we also get below the 500MB mark of disk usage:

df -h --output=source,used,target /
Filesystem      Used Mounted on
/dev/sda1       495M /
free -ht | grep ^Total
Total:  240M  21M 96M

Using dropbear

dropbear is a lightweight SSH server designed for small memory environments. We can install it by running:

apt install --no-install-recommends dropbear-run

We must also enable the service so it starts during boot:

sed -i 's,^NO_START=1,NO_START=0,' /etc/default/dropbear

Now, we can remove OpenSSH:

apt purge --auto-remove ssh

Going really minimal

If we are really short on disk space, we can run debootstrap with the --variant=minbase option:

sudo debootstrap --arch amd64 --variant=minbase stretch /mnt/debian http://deb.debian.org/debian

This option, according to debootstrap’s manual page, only installs the essential packages:

Currently, the variants supported are minbase, which only includes essential packages and apt; buildd, which installs the build-essential packages into TARGET; and fakechroot, which installs the packages without root privileges. The default, with no --variant=X argument, is to create a base Debian installation in TARGET.

Manpages

Along with the usual debootstrap configuration, in order to have a functional environment, we need to install an init system (such as SysV) and some network tools so we can connect to the network:

apt install --no-install-recommends ifupdown iproute2 isc-dhcp-client netbase

In the end, we should be able to shave off a few megabytes of disk space by having less packages than the default debootstrap variant, and also use less memory:

df -h --output=source,used,target /
Filesystem      Used Mounted on
/dev/sda1       453M /
free -ht | grep ^Total
Total:  240M  19M 182M
dpkg -l | grep ^ii | wc -l
118

References