Debian with Read-Only Root


The descibed procedure shows how to install a Debian based minimal linux system with read-only root filesystem on, e.g. a CompactFlash (CF) card or USB drive.

For the installation you need an already running Debian system with an architecture that is compatible with the architecture of the final system (e.g., a Debian on an amd64 PC, if the final system is a thin client with amd64 or i686 architecture). This system is required in order to prepare the CF card or USB drive. All of the following steps are done under this system.


The following steps are all done as user root. You can easily damage your running system with a wrong command. Be careful and understand the instructions before trying them. USE ON YOUR OWN RISK!

Preparation of the Filesystem

The Debian system requires at least 512 MB. The USB drive has to be partitioned and formated. Let us assume the drive is /dev/sdd. Of course, you will have to change this to the appropriate device on your system.

WARNING: The next steps will destroy any data on your USB drive!

Partition /dev/sdd, for example, with fdisk or cfdisk:

  1. delete all existing partitions
  2. create a new, first primary partition (starting at the begining of the device); recommended size: at least 1 GB
  3. set the partition's system ID to 0x83
  4. set the bootable flag for this partition

Next, format the partition with the EXT2 filesystem. As the filesystem will be read-only, we do not gain anything using a journalling filesystem.

# mkfs.ext2 /dev/sdd1

Finally, mount the new filesystem:

# mount /dev/sdd1 /mnt

Installation of the System

Install the following tool:

# apt-get install debootstrap

The Debian system for the USB drive is now bootstrapped to a temporary directory /scratch/deb and then copied to the USB drive mounted on /mnt:

# mkdir /scratch/deb
# debootstrap --arch=amd64 wheezy /scratch/deb
# cp -a /scratch/deb/* /mnt

Basic Configuration

Now, several files under /etc must be writable for a functional system (see ReadonlyRoot in the Debian Wiki) as well as further directories under /. There are several ways how to solve this. One way is to resolve all the issues for the files under /etc manually as described in the Debian Wiki. Another way is to transparently overlay an aufs filesystem as a writable layer and make / appear writable (see aufsRootFileSystemOnUsbFlash in the Ubuntu Documentation). Any changes are held in RAM and lost on the next reboot.

Here, we use a mixture of these approaches and only use an aufs filesystem for /etc and /var. This can be configured as shown in the following.

First of all, the file /etc/mtab normally needs to be writable. As an alternative, it may be substituted by a symlink:

# rm /cf/etc/mtab
# ln -s /proc/mounts /mnt/etc/mtab

Then, create the following file:

File: /mnt/etc/fstab

UUID=01234567-89ab-cdef-0123-456789abcdef / ext2 defaults,noatime,ro 0 0
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0

Here, 01234567-89ab-cdef-0123-456789abcdef has to be replaced by the block ID of /dev/sdd1, which can be read using:

# blkid /dev/sdd1

Create the following shell script, which will make /etc and /var writable:

File: /mnt/etc/init.d/

#! /bin/sh
# Provides:          mountaufs
# Required-Start:    mountall
# Required-Stop:
# Should-Start:
# Default-Start:     S
# Default-Stop:
# Short-Description: Mount aufs over /etc and /var.
# Description:


. /lib/init/
. /lib/lsb/init-functions

make_rw () {
  mkdir "/tmp/${1}"
  mount -t aufs -o "br:/tmp/${1}=rw:/${1}=ro" none "/${1}"

do_start () {
  make_rw etc
  make_rw var

case "$1" in
    echo "Error: argument '$1' not supported" >&2
    exit 3
    # No-op
    echo "Usage: [start|stop]" >&2
    exit 3


Make sure it is executable:

# chmod +x /mnt/etc/init.d/

Basic Network Configuration

The hostname of the system is set to client like this:

File: /mnt/etc/hostname


Static hostnames can be configured:

File: /mnt/etc/hosts localhost
## the following line only, if the system will have a static IP: client

::1       localhost ip6-localhost ip6-loopback
fe00::0   ip6-localnet
ff00::0   ip6-mcastprefix
ff02::1   ip6-allnodes
ff02::2   ip6-allrouters
ff02::3   ip6-allhosts

The network interface configuration can be either static ...

File: /mnt/etc/network/interfaces

auto lo
iface lo inet loopback

allow-hotplug eth0
iface eth0 inet static
        # dns-* options are implemented by the resolvconf package, if installed

File: /mnt/etc/resolv.conf.static


... or dynamic:

File: /mnt/etc/network/interfaces

auto lo
iface lo inet loopback

allow-hotplug eth0
iface eth0 inet dhcp

Installation of Kernel and Bootloader

We now change root to the bootstrapped system to install further packages, especially, the kernel and bootloader:

# mount -t proc proc /mnt/proc
# mount -o bind /dev /mnt/dev
# mount -o bind /sys /mnt/sys
# LC_ALL="C" chroot /mnt /bin/bash

The installation is done as usual:

# apt-get update
# apt-get install linux-image-amd64 grub2 aufs-tools
# apt-get clean

During the installation of the grub2 package, it will ask you where to install the bootloader. Answer with /dev/sdd.

If you did not check anything and skip the installation of the bootloader in the previous step, you can still manually install it:

# grub-install /dev/sdd

Finishing the Configuration

Activate the previously generated start script /etc/init.d/

# update-rc.d enable

Finally, exit the chroot environment.

# exit
# umount /mnt/proc
# umount /mnt/dev
# umount /mnt/sys
# umount /mnt

In case you encounter problems umounting /mnt, you might need to re-enter the USB drive system and stop processes still accessing it. You can find such processes with:

# fuser -m /dev/sdd1

Done. The USB drive should now work in the thin client. Of course, you can also add further programs by changing root to the USB drive's filesystem.

Note: The current system has no root password. You might want to add one in the change root environment with the passwd command.